Connect with us

Education

Reverse Engineering a North Korean Sim City Game

Mish Boyka

Published

on

Reverse Engineering a North Korean Sim City Game – Digital NK

Reverse engineering the North Korean version of a popular Sim City-like game using Ghidra and ndSpy to understand video game monetization strategies in the DPRK and the marketization of the country’s economy.

Key takeaways:

  • Android devices and applications are increasingly common in North Korea. Physical “app stores” can be found on every street corner in Pyongyang.
  • The game considered in this post is based on a Chinese version of a popular Android game developed in the Netherlands
  • The game’s monetization strategy was adapted to the country’s infrastructure (low internet/intranet availability, physical app stores)
  • The North Korean version eschews the original freemium + online microtransaction model for a one-time licence purchase + offline microtransaction model
  • File integrity checks added by North Korean developers shows that piracy is a concern and suggests the existence a warez/cracking scene in the DPRK
  • The cryptographic algorithms used for the licence are MD5, SHA1, RSA and AES. The library used by the game included the domestically developed private key algorithms Pilsung and Jipsam, but they were not used as part of the licencing system

0. Introduction
1. Licensing system
2. File integrity checks
3. In-game monetization strategy and key generation
4. Conclusion

0. Introduction

During a recent trip to North Korea, I noticed the recent and ubiquitous presence of Information Technology Exchange Rooms (정보기술교류실), physical stores where one can purchase a variety of electronic devices – from laptops and tablets to USB sticks and chargers – as well as software and video games for PC, mobile and tablets (for an in-depth look at what goes on inside those stores as well as what the app selection looks like, this article by Alek Sigley provides a an excellent description. There are also a few videos on YouTube). After looking through the catalogue of available games at different stores, I eventually decided to try and buy a Sim City-like game called City Management (도시경경).

Billboards for various app stores in Pyongyang

The game only cost 5000 wons (less than 1 USD) which I paid to have the app installed on the phone I had, a Samsung Galaxy A5 running Android 8. The vendor connected the phone to his PC, transferred the APK and tried to install it, but to no avail. After multiple attempts, he eventually informed me that North Korean apps most likely could not run on phones from other countries.

Advertisement for a car racing game inside a North Korean app store

Fortunately, I was later able to purchase one of the different tablets sold in North Korea. I got the Morning (아침) brand, which is geared towards students and quite affordable. The tablet ran Android 4 (Kit Kat) on an ARM cpu and came loaded with a few educational apps: language learning courses, dictionaries and several e-book libraries containing the complete works of Kim Il Sung, school textbooks and a collection of literary works. No games, but that could now be fixed quite easily.

A North Korean Ach’im (Morning) tablet

I retrieved the City Management APK from my phone and installed it on the tablet, where it ran perfectly. Unfortunately, after the game’s initial splash screen, I landed on this:

Licence key needed

The screen tells us that there is no “key file” (열쇠화일) and that we should purchase one at a store. There is a “request number” (요청번호) likely used to generate the licence key and make sure it can’t be shared with other devices. Unfortunately, since the APK never installed, the vendor did not put a licence file on my phone when I bought the app. My stay in North Korea was coming to an end too and I did not have time to go back to an app store to buy a new key. So I figured I would take a look inside the app and see if I could get it running nonetheless.


1. Licensing system

To start looking into the APK’s code, I’ll use the standard suite of tools to decompress, decompile and rebuild android apps: dex2jar, jd-gui, apktool and apksign. I’ll also use Android Studio to run and debug the app. The fact that I couldn’t run the app on my phone may have just come from an Android version compatibility issue: I had no problem running it on an emulated Android 4.4 device with Android Studio. The decompilation of the classes.dex file gives us some interesting information right away:

Packages and classes from the decompiled classes.dex file

The name of the com.bz.cityisland2 package actually refers to the original game that City Management is based on: City Island 2 by the Dutch game studio Sparkling Society. The name of the package com.smartions.appprotected refers to Smartions, a company that offers solutions to “monetize your mobile game or app in China” and are apparently also City Island’s distributor in China. There are no mentions of those companies in the game itself however. The game’s loading splash screen only tells us that the game was made by the Ryusong (meteor) Technology Exchange Center (류성기술교류소) and that it is protected by the law for the protection of software (콤퓨터쏘프트웨어보호법). The law has been in place since 2003 to regulate the sales and distribution of software in the country and guarantees software developers the private ownership of their creation.

Loading screen for the game

It’s hard to tell whether the North Korean version is based on the source code of the original game or if it’s entirely reverse engineered. In any case, the North Korean version does not use Smartions’s monetization system nor Sparkling Society’s but relies on a different system, which is the main difference from the original game. Save for the translation and some minor renames, the game is otherwise similar to the original (from a cursory examination) in its design, gameplay, features… to the original.

Structure a Unity APK. From Shim et al., Static and Dynamic GFN of Android Malware and Goodware Written with Unity Framework (2018).

There’s not much more we can glean from the Java code for now since, as the classes in unity3dplayer and AndroidManifest.xml file make clear, it is used to run code that was written with Unity, a popular cross-plaform video game framework which uses C# as its main programming language. The Unity code is stored in various library with the developer’s C# code being compiled to Assembly-CSharp.dll. C# compiled code is easily decompilable using tools such as dnSpy. Once the dll is decompiled, we can look for the message we got earlier “열쇠화일이 존재하지 않습니다” (“The key file does not exist”) to find the bits of code we are interested in. The string search takes us to the CIGLoadingScreen class where we find the string among other variables:

	// Token: 0x0400056A RID: 1386
	private string userKey;

	// Token: 0x0400056B RID: 1387
	private string tapjoyCurrencyIdentifier;

	// Token: 0x0400056C RID: 1388
	private bool bannerVisible;

	// Token: 0x0400056D RID: 1389
	private int _loadingScreenShownCount;

	// Token: 0x0400056E RID: 1390
	private Dictionary<int, bool> m_gameObjectStatus = new Dictionary<int, bool>();

	// Token: 0x0400056F RID: 1391
	private bool m_isVerify;

	// Token: 0x04000570 RID: 1392
	private Font kfont;

	// Token: 0x04000571 RID: 1393
	private string reqMsg = "열쇠화일이 존재하지 않습니다.rn열쇠화일을 판매소에서 구입하십시오.";

	// Token: 0x04000572 RID: 1394
	private string reqNumLabel = "요청번호 : ";

	// Token: 0x04000573 RID: 1395
	private string reqNum;

	// Token: 0x04000574 RID: 1396
	private string finishLabel = "끝내기";

Looking for the name of the string variable reqMsg takes us here:

	// Token: 0x06000928 RID: 2344 RVA: 0x00026E60 File Offset: 0x00025060
	private void OnGUI()
	{
		if (!this.m_isVerify && this.loadingDone)
		{
			GUI.skin.font = this.kfont;
			GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), this.blackBg, ScaleMode.StretchToFill);
			GUI.Label(this.GetTextLabelRect(this.reqMsg, 0.5f, 0.3f), this.reqMsg);
			GUI.Label(this.GetTextLabelRect(this.reqNumLabel, 0.3f, 0.5f), this.reqNumLabel);
			GUI.Label(this.GetTextLabelRect(this.reqNum, 0.6f, 0.5f), this.reqNum);
			RectOffset padding = GUI.skin.button.padding;
			GUI.skin.button.padding = new RectOffset(20, 20, 10, 10);
			if (GUI.Button(this.GetButtonRect(this.finishLabel, 0.5f, 0.8f), this.finishLabel))
			{
				Application.Quit();
			}
		}
	}

This is the code used to display the splashscreen we encountered earlier. If the boolean property this.m_isVerify, presumably the result of a call to a function checking the existence and validity of a licence key, is False then, the screen is displayed with the message we saw earlier and the “request number”. The verification function and the generation of the request number are handled in another class GameCus:

using System;
using System.IO;
using System.Runtime.InteropServices;

// Token: 0x02000147 RID: 327
public class GameCus
{
	// Token: 0x06000AA7 RID: 2727
	[DllImport("Game")]
	private static extern int vProcess(byte[] key, int keyLen, byte[] certData, int certDataLen);

	// Token: 0x06000AA9 RID: 2729 RVA: 0x0002E910 File Offset: 0x0002CB10
	public string GetReqNumber()
	{
		string deviceIdString = this.GetDeviceIdString();
		return string.Format("{0:d4} {1:d4} {2:d4} {3:d4}", new object[]
		{
			deviceIdString.Substring(0, 4),
			deviceIdString.Substring(4, 4),
			deviceIdString.Substring(8, 4),
			deviceIdString.Substring(12, 4)
		});
	}

	// Token: 0x06000AAA RID: 2730 RVA: 0x0002E968 File Offset: 0x0002CB68
	public string GetDeviceIdString()
	{
		string text = Utils.GetDeviceModel();
		text = string.Format("{0:d10}", (uint)text.GetHashCode());
		string str = text.Substring(2, 8);
		string text2 = Utils.GetDeviceUid();
		text2 = string.Format("{0:d10}", (uint)text2.GetHashCode());
		string str2 = text2.Substring(2, 8);
		return str + str2;
	}

	// Token: 0x06000AAB RID: 2731 RVA: 0x0002E9CC File Offset: 0x0002CBCC
	public bool checkCertData(byte[] certData)
	{
		if (certData == null || certData.Length == 0)
		{
			return false;
		}
		string text = this.GetDeviceIdString() + "-evXww9A+fJxc7IOv93ZMlvonEtE";
		char[] array = text.ToCharArray();
		byte[] array2 = new byte[array.Length];
		for (int i = 0; i < array.Length; i++)
		{
			array2[i] = (byte)array[i];
		}
		return GameCus.vProcess(array2, array2.Length, certData, certData.Length) == 4097;
	}

	// Token: 0x06000AAC RID: 2732 RVA: 0x0002EA30 File Offset: 0x0002CC30
	public byte[] getCertData()
	{
		string keyFilePath = Utils.GetKeyFilePath("103107002.rsb");
		if (keyFilePath == null)
		{
			return null;
		}
		byte[] result;
		try
		{
			if (!File.Exists(keyFilePath))
			{
				result = null;
			}
			else
			{
				FileStream fileStream = File.Open(keyFilePath, FileMode.Open);
				byte[] array = new byte[fileStream.Length];
				if ((long)fileStream.Read(array, 0, (int)fileStream.Length) != fileStream.Length)
				{
					result = null;
				}
				else
				{
					result = array;
				}
			}
		}
		catch (Exception ex)
		{
			ex.ToString();
			result = null;
		}
		return result;
	}
}

There are two things going on here, one being the generation of the request number and the other the verification of the licence key file.

The algorithm for the generation of the request number in GetReqNumber and GetDeviceIdString is fairly straightforward. We first get the device’s model name and the device’s unique id as two strings from calling two functions in the Utils package (not detailed here). We get the hash code for each of these strings and convert it to its base 10 representation as a string. The string is padded with leading 0’s if the length of the number is smaller than 10. Then the 2nd to 9th characters of each string are concatenated and split into 4 space separated blocks of 4 characters each. The resulting string is the “request number” (요청번호) that we are supposed to give to the app store to get a licence key.

The licence key verification process is a bit more complicated. First, getCertData will retrieve the licence key and read the data stored in it. We can see that the licence key’s filename is 103107002.rsb but to find its exact location, we need to look into the Utils package:

	// Token: 0x060003FC RID: 1020 RVA: 0x000109FC File Offset: 0x0000EBFC
	public static string GetKeyFilePath(string keyFileName)
	{
		string result = null;
		using (AndroidJavaObject @static = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
		{
			using (AndroidJavaClass androidJavaClass = new AndroidJavaClass("com.unity3d.player.kk"))
			{
				object[] args = new object[]
				{
					@static,
					"/GameRyusong/" + keyFileName
				};
				result = androidJavaClass.CallStatic<string>("a2", args);
			}
		}
		return result;
	}

Here the name of the key file is concatenated to the name of a directory called /GameRyusong/ and that string is sent to a function defined back in the Java part of the app. Going back to our decompiled Java code, we can find that function (a2 in the com.unity3d.player.kk class). Unfortunately, the decompiler was unable to process the function and all we have is Java bytecode:

  /* Error */
  public static String a2(Context paramContext, String paramString)
  {
    // Byte code:
    //   0: iconst_0
    //   1: istore_2
    //   2: new 42	java/util/ArrayList
    //   5: dup
    //   6: invokespecial 43	java/util/ArrayList:<init>	()V
    //   9: astore_3
    //   10: aload_3
    //   11: invokestatic 49	android/os/Environment:getExternalStorageDirectory	()Ljava/io/File;
    //   14: invokevirtual 54	java/io/File:getAbsolutePath	()Ljava/lang/String;
    //   17: invokevirtual 58	java/util/ArrayList:add	(Ljava/lang/Object;)Z
    //   20: pop
    //   21: new 60	java/lang/ProcessBuilder
    //   24: dup
    //   25: iconst_0
    //   26: anewarray 62	java/lang/String
    //   29: invokespecial 65	java/lang/ProcessBuilder:<init>	([Ljava/lang/String;)V
    //   32: iconst_1
    //   33: anewarray 62	java/lang/String
    //   36: dup
    //   37: iconst_0
    //   38: ldc 67
    //   40: aastore
    //   41: invokevirtual 71	java/lang/ProcessBuilder:command	([Ljava/lang/String;)Ljava/lang/ProcessBuilder;
    //   44: iconst_1
    //   45: invokevirtual 75	java/lang/ProcessBuilder:redirectErrorStream	(Z)Ljava/lang/ProcessBuilder;
    //   48: invokevirtual 79	java/lang/ProcessBuilder:start	()Ljava/lang/Process;
    //   51: astore 16
    //   53: aload 16
    //   55: astore 6
    //   57: aload 6
    //   59: invokevirtual 85	java/lang/Process:waitFor	()I
    //   62: pop
    //   63: aload 6
    //   65: invokevirtual 89	java/lang/Process:getInputStream	()Ljava/io/InputStream;
    //   68: astore 19
    //   70: sipush 1024
    //   73: newarray <illegal type>
    //   75: astore 20
    //   77: ldc 91
    //   79: astore 9
    //   81: aload 19
    //   83: aload 20
    //   85: invokevirtual 97	java/io/InputStream:read	([B)I
    //   88: iconst_m1
    //   89: if_icmpne +48 -> 137
    //   92: aload 19
    //   94: invokevirtual 100	java/io/InputStream:close	()V
    //   97: aload 6
    //   99: invokevirtual 103	java/lang/Process:destroy	()V
    //   102: aload 9
    //   104: ldc 105
    //   106: invokevirtual 109	java/lang/String:split	(Ljava/lang/String;)[Ljava/lang/String;
    //   109: astore 10
    //   111: iconst_0
    //   112: istore 11
    //   114: iload 11
    //   116: aload 10
    //   118: arraylength
    //   119: if_icmplt +93 -> 212
    //   122: aload_3
    //   123: invokevirtual 112	java/util/ArrayList:size	()I
    //   126: istore 15
    //   128: iload_2
    //   129: iload 15
    //   131: if_icmplt +126 -> 257
    //   134: ldc 91
    //   136: areturn
    //   137: new 29	java/lang/StringBuilder
    //   140: dup
    //   141: aload 9
    //   143: invokestatic 116	java/lang/String:valueOf	(Ljava/lang/Object;)Ljava/lang/String;
    //   146: invokespecial 33	java/lang/StringBuilder:<init>	(Ljava/lang/String;)V
    //   149: new 62	java/lang/String
    //   152: dup
    //   153: aload 20
    //   155: invokespecial 119	java/lang/String:<init>	([B)V
    //   158: invokevirtual 37	java/lang/StringBuilder:append	(Ljava/lang/String;)Ljava/lang/StringBuilder;
    //   161: invokevirtual 40	java/lang/StringBuilder:toString	()Ljava/lang/String;
    //   164: astore 21
    //   166: aload 21
    //   168: astore 9
    //   170: goto -89 -> 81
    //   173: astore 7
    //   175: aconst_null
    //   176: astore 6
    //   178: aload 7
    //   180: astore 8
    //   182: ldc 91
    //   184: astore 9
    //   186: aload 8
    //   188: invokevirtual 122	java/lang/Exception:printStackTrace	()V
    //   191: aload 6
    //   193: invokevirtual 103	java/lang/Process:destroy	()V
    //   196: goto -94 -> 102
    //   199: astore 5
    //   201: aconst_null
    //   202: astore 6
    //   204: aload 6
    //   206: invokevirtual 103	java/lang/Process:destroy	()V
    //   209: aload 5
    //   211: athrow
    //   212: aload 10
    //   214: iload 11
    //   216: aaload
    //   217: ldc 124
    //   219: invokevirtual 109	java/lang/String:split	(Ljava/lang/String;)[Ljava/lang/String;
    //   222: astore 12
    //   224: iconst_0
    //   225: istore 13
    //   227: iload 13
    //   229: aload 12
    //   231: arraylength
    //   232: if_icmplt +9 -> 241
    //   235: iinc 11 1
    //   238: goto -124 -> 114
    //   241: aload_3
    //   242: aload 12
    //   244: iload 13
    //   246: aaload
    //   247: invokevirtual 58	java/util/ArrayList:add	(Ljava/lang/Object;)Z
    //   250: pop
    //   251: iinc 13 1
    //   254: goto -27 -> 227
    //   257: new 51	java/io/File
    //   260: dup
    //   261: new 29	java/lang/StringBuilder
    //   264: dup
    //   265: aload_3
    //   266: iload_2
    //   267: invokevirtual 128	java/util/ArrayList:get	(I)Ljava/lang/Object;
    //   270: checkcast 62	java/lang/String
    //   273: invokestatic 116	java/lang/String:valueOf	(Ljava/lang/Object;)Ljava/lang/String;
    //   276: invokespecial 33	java/lang/StringBuilder:<init>	(Ljava/lang/String;)V
    //   279: aload_1
    //   280: invokevirtual 37	java/lang/StringBuilder:append	(Ljava/lang/String;)Ljava/lang/StringBuilder;
    //   283: invokevirtual 40	java/lang/StringBuilder:toString	()Ljava/lang/String;
    //   286: invokespecial 129	java/io/File:<init>	(Ljava/lang/String;)V
    //   289: invokevirtual 133	java/io/File:exists	()Z
    //   292: ifeq +29 -> 321
    //   295: new 29	java/lang/StringBuilder
    //   298: dup
    //   299: aload_3
    //   300: iload_2
    //   301: invokevirtual 128	java/util/ArrayList:get	(I)Ljava/lang/Object;
    //   304: checkcast 62	java/lang/String
    //   307: invokestatic 116	java/lang/String:valueOf	(Ljava/lang/Object;)Ljava/lang/String;
    //   310: invokespecial 33	java/lang/StringBuilder:<init>	(Ljava/lang/String;)V
    //   313: aload_1
    //   314: invokevirtual 37	java/lang/StringBuilder:append	(Ljava/lang/String;)Ljava/lang/StringBuilder;
    //   317: invokevirtual 40	java/lang/StringBuilder:toString	()Ljava/lang/String;
    //   320: areturn
    //   321: iinc 2 1
    //   324: goto -196 -> 128
    //   327: astore 5
    //   329: goto -125 -> 204
    //   332: astore 17
    //   334: aload 17
    //   336: astore 8
    //   338: ldc 91
    //   340: astore 9
    //   342: goto -156 -> 186
    //   345: astore 8
    //   347: goto -161 -> 186
    // Local variable table:
    //   start	length	slot	name	signature
    //   0	350	0	paramContext	Context
    //   0	350	1	paramString	String
    //   1	321	2	i	int
    //   9	291	3	localArrayList	java.util.ArrayList
    //   199	11	5	localObject1	Object
    //   327	1	5	localObject2	Object
    //   55	150	6	localProcess1	Process
    //   173	6	7	localException1	Exception
    //   180	157	8	localObject3	Object
    //   345	1	8	localException2	Exception
    //   79	262	9	localObject4	Object
    //   109	104	10	arrayOfString1	String[]
    //   112	124	11	j	int
    //   222	21	12	arrayOfString2	String[]
    //   225	27	13	k	int
    //   126	6	15	m	int
    //   51	3	16	localProcess2	Process
    //   332	3	17	localException3	Exception
    //   68	25	19	localInputStream	java.io.InputStream
    //   75	79	20	arrayOfByte	byte[]
    //   164	3	21	str	String
    // Exception table:
    //   from	to	target	type
    //   21	53	173	java/lang/Exception
    //   21	53	199	finally
    //   57	77	327	finally
    //   81	97	327	finally
    //   137	166	327	finally
    //   186	191	327	finally
    //   57	77	332	java/lang/Exception
    //   81	97	345	java/lang/Exception
    //   137	166	345	java/lang/Exception
  }

This isn’t the most readable bit of code, but we can at least surmise from it that it looks for the keyfile in the device’s primary shared storage directory and if it finds it, returns the absolute path of the keyfile (or 0 otherwise). The rest of getCertData merely reads the key file’s contents and returns it as an array of bytes. That array of bytes can then be passed as an argument when calling checkCertData.

checkCertData takes our request number before formatting (i.e. without separating spaces), adds the trailing string “-evXww9A+fJxc7IOv93ZMlvonEtE” to it and sends the result along with the content of the licence key to a function called vProcess. If the call to that function returns 4097, the licence key is valid. We now need to look into the vProcess function, but that function is not part of our Unity C# code. Rather it is imported from an external library:

public class GameCus
{
	// Token: 0x06000AA7 RID: 2727
	[DllImport("Game")]
	private static extern int vProcess(byte[] key, int keyLen, byte[] certData, int certDataLen);

The library can be found as libGame.so in the /lib directory of the APK. This particular library is written in C++ so we can’t decompile it as easily as C#. Fortunately the NSA recently released its reverse engineering tool Ghidra which works great for disassembling binaries and even has an option to decompile to pseudo-C code. It won’t be as neat and readable as a C# decompile, but it can certainly help reading and understanding the assembly code (especially if you’re more familiar with x86 ASM than ARM ASM!):

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined vProcess()
                               assume LRset = 0x0
                               assume TMode = 0x1
             undefined         r0:1           <RETURN>
             undefined4        Stack[-0x10]:4 local_10                                XREF[1]:     000b3b58(W)  
                             vProcess                                        XREF[2]:     Entry Point(*), 
                                                                                          vInit:000b3b92(c)  
        000b3b50 13 b5           push       { r0, r1, r4, lr }
        000b3b52 04 4c           ldr        r4,[DAT_000b3b64]                                = 0002C2C0h
        000b3b54 7c 44           add        r4,pc
        000b3b56 24 68           ldr        r4,[r4,#0x0]=>->license_key                      = 000efe78
        000b3b58 00 94           str        r4=>license_key,[sp,#0x0]=>local_10              = 
        000b3b5a ff f7 8d ff     bl         generalProcess                                   undefined generalProcess(undefin
        000b3b5e 02 b0           add        sp,#0x8
        000b3b60 10 bd           pop        { r4, pc }
        000b3b62 00              ??         00h
        000b3b63 bf              ??         BFh
                             DAT_000b3b64                                    XREF[1]:     vProcess:000b3b52(R)  
        000b3b64 c0 c2 02 00     undefined4 0002C2C0h                                        ?  ->  0002c2c0

Here the dissassembly is enough and actually more instructive than the pseudo-C decompile (which merely renders the call to generalProcess(); and misses the license_key argument). We load the value 0x2C2C0 from DAT_000b3b64, add that value to the program counter (pc) register, this gives us an address pointing license_key which we load into r4 before calling generalProcess. The license_key value looks like this:

                             license_key                                     XREF[3]:     Entry Point(*), 
                                                                                          vProcess:000b3b58(*), 
                                                                                          000dfe18(*)  
        000efe78 00 04 a3        undefine
                 a0 75 53 
                 92 1d ee 
           000efe78 00              undefined100h                     [0]                               XREF[3]:     Entry Point(*), 
                                                                                                                     vProcess:000b3b58(*), 
                                                                                                                     000dfe18(*)  
           000efe79 04              undefined104h                     [1]
           000efe7a a3              undefined1A3h                     [2]
           000efe7b a0              undefined1A0h                     [3]
[...]
           000efef2 da              undefined1DAh                     [122]
           000efef3 27              undefined127h                     [123]
           000efef4 05              undefined105h                     [124]
           000efef5 5a              undefined15Ah                     [125]
           000efef6 e6              undefined1E6h                     [126]
           000efef7 4e              undefined14Eh                     [127]
           000efef8 c5              undefined1C5h                     [128]
           000efef9 f7              undefined1F7h                     [129]
           000efefa 00              undefined100h                     [130]
           000efefb 00              undefined100h                     [131]
           000efefc 00              undefined100h                     [132]
[...]
           000eff76 00              undefined100h                     [254]
           000eff77 01              undefined101h                     [255]
           000eff78 00              undefined100h                     [256]
           000eff79 01              undefined101h                     [257]
   

To put it differently, license_key is an array of 258 bytes. Towards the end of the array, after a long series of empty bytes, we find the value 0x10001 in the three final bytes. That value might seem familiar if you’ve worked with public-key encryption before: it is 65537 in base 10, a commonly chosen e or public exponent for the RSA algorithm. The previous value could be a public key n, which would be 1028 bits in size. Let’s see if we can validate that assumption by looking into the generalProcess function.

Fortunately, Ghidra’s decompilation module works better here and offers a more readable output than plain ARM assembly:

generalProcess(undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
              void *param_5)

{
  int iVar1;
  undefined4 uVar2;
  int iVar3;
  int local_228;
  undefined4 local_224;
  undefined auStack544 [16];
  undefined auStack528 [96];
  undefined auStack432 [128];
  undefined auStack304 [260];
  int local_2c;
  
  local_2c = __stack_chk_guard;
  local_228 = 0;
  memcpy(auStack304,param_5,0x102);
  iVar1 = ReadBlock(auStack432,&local_224,0x80,param_3,param_4);
  if (iVar1 == 0) {
    iVar1 = R_VerifyInit(auStack528,5);
    if (iVar1 == 0) {
      iVar3 = iVar1;
      do {
        iVar1 = ReadUpdate(param_1,iVar3,param_2,auStack544,&local_228,0x10);
        if (iVar1 != 0) {
          iVar1 = R_VerifyFinal(auStack528,auStack432,local_224,auStack304);
          break;
        }
        iVar3 = iVar3 + local_228;
        iVar1 = R_VerifyUpdate(auStack528,auStack544);
      } while (iVar1 == 0);
    }
  }
  else {
    iVar1 = 0;
  }
  R_memset(auStack528,0,0x60);
  R_memset(auStack544,0,0x10);
  uVar2 = 0x1001;
  if (iVar1 != 0) {
    uVar2 = 0x2000;
  }
  if (local_2c == __stack_chk_guard) {
    return uVar2;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

None of the functions we have here mention RSA explicitly, but a quick look inside R_VerifyFinal reveals a call to an RSAPublicDecrypt function. The function names and the code itself all look like they were taken from the RSAREF implementation of RSA released by RSA Labs in the 1990’s. Compare, for instance, the code above, with the sample signature verification function of RSAREF:

static void DoVerifyFile ()
{
  FILE *file;
  R_RSA_PUBLIC_KEY *publicKey;
  R_SIGNATURE_CTX context;
  int digestAlgorithm, status;
  unsigned char partIn[16], signature[MAX_SIGNATURE_LEN];
  unsigned int partInLen, signatureLen;

  status = 0;

  if (ReadInit (&file, "  Enter name of file to verify"))
    return;

  do {
    if (GetPublicKey (&publicKey))
      break;

    if (GetDigestAlgorithm (&digestAlgorithm))
      break;

    if (ReadBlock
        (signature, &signatureLen, sizeof (signature),
         "  Enter filename of signature"))
      break;

    if ((status = R_VerifyInit (&context, digestAlgorithm)) != 0)
      break;

    while (!ReadUpdate (file, partIn, &partInLen, sizeof (partIn)))
      if ((status = R_VerifyUpdate (&context, partIn, partInLen)) != 0)
        break;
    if (status)
      break;

    if ((status = R_VerifyFinal
         (&context, signature, signatureLen, publicKey)) != 0)
      break;

    PrintMessage ("Signature verified.");
  } while (0);

  ReadFinal (file);

  if (status)
    PrintError ("verifying file", status);

  R_memset ((POINTER)&context, 0, sizeof (context));
  R_memset ((POINTER)partIn, 0, sizeof (partIn));
}

In our case, R_VerifyInit sets up a context and specifies MD5 as the hashing algorithm to use with the second argument, we then compute a digest by iterating over the data and updating the digest with R_VerifyUpdate before verifying the signature with R_VerifyFinal. If the signature is verified, the value 0x1001 (the 4097 that the C# code used to compare to the return value of the call to vProcess) otherwise 0x2000 is returned.

Knowing that the implementation draws upon RSAREF also allows us to extract more information about the RSA keys: the license_key is likely to be of the type R_RSA_PUBLIC_KEY defined in RSAREF as follows:

typedef struct {
  unsigned int bits;                           /* length in bits of modulus */
  unsigned char modulus[MAX_RSA_MODULUS_LEN];                    /* modulus */
  unsigned char exponent[MAX_RSA_MODULUS_LEN];           /* public exponent */
} R_RSA_PUBLIC_KEY;

So the first 4 bytes of the license_key correspond to the length of the public key in bytes, i.e. 0x0400 or 1024 bits. The exponent, as we already know, is at the end and has a value of 65537. The private key then is what remains and its value is:

A3A07553921DEEAC5AC66EE02D3C0B3A130595D5BBE759DCF4D9D7F9D19F84CF2515F548B02730A8E55A089EB38D58253F15051FFA2A8EAD55BDFD978E71280E104BA4A21AA3DEB8318FC5A5DA9C5EF92FB1687B671524C33C8094ECC199AA8FD899A729578D433941BC6E3B6AA909AD6589D68213A32722DA27055AE64EC5F7

This is not particularly good news. A public key of 1024bits is currently still too large to be factorized by a single individual such as myself, unless the implementation is flawed or the pseudo-random number generator used to create the key present a weakness. The implementation used here does seem a bit dated. MD5 is not considered secure anymore. The RSAREF implementation is over 20 year old. Looking around the library, one notices that it also uses an old version of OpenSSL (OpenSSL 0.9.8g from October 2007). Interestingly, in addition to the commonly supported algorithms, the OpenSSL used here also ships with Jipsam and Pilsung, two North Korean private key algorithms described in further detail here. This tells us that we’re not dealing with just a translated version of the game, but that the whole licensing system was designed within the DPRK.

Part of the Jipsam encryption algorithm, as seen from Ghidra

However, we know the message whose signature will be verified (the “request number” plus a suffix) and do not need to tamper with it, so finding MD5 collisions won’t be of any help. Several security weaknesses have been found in older version of OpenSSL, including with the PRNG, but there is no indication that it was used to generate the RSA keys. Indeed since the RSA signature verification function uses RSAREF, the same library was most likely also used to generate the keys. The RSAREF’s PRNG was actually audited by Bruce Schneier, but the vulnerabilities found (timing attacks) are of no help to us. Checking for basic vulnerabilities such as small or known factors using common attacks and tools like RsaCtf or factordb yields no results.

We are unable to factor the RSA public key to generate a licence for our device’s request number but we still have a few options if we want to play the game:

  • Tamper with the RSA parameters by modifying libGame.so. For instance, we can change the private key to one we’ve generated and whose factor we know.
  • Modify the generalProcess function in libGame.so so that it always returns 4097 and appears to validate the signature to the C# code.
  • Modify the C# code to change the value returned after the call to vProcess.

None of these options are particularly hard to implement. The first one would allow us to generate our own licence keys and would be more elegant but also more time-consuming, while the last two are trivial.


2. File integrity checks

I went with the last option of directly modifying the C# code in ndSpy as it is the most straightforward. We simply go back to the checkCertData method of the GameCus class and change the line:

return GameCus.vProcess(array2, array2.Length, certData, certData.Length) == 4097;

into:

return true;

before recompiling the dll with ndSpy.

We then rebuild the apk with apktool and sign it with signapk before installing it on our emulated Android device. The patch seems to have worked as instead of being asked for a licence key, we are taken to a screen with a button to start (시작) the game:

Start screen

The game loads (정재중) for a while and just when we get a glimpse of the first level, it crashes. After running the game again in debug mode to try to catch the exception, Android Studio’s debugger breaks and tells us that the crash happened in the Unity part of the code while attempting to call a function named vInit:

Android Studio’s debugger breaks when the game crashes after it was patched

Looking for vInit in the Unity libraries of the APK takes us to UnityEngine.dll and the MonoBehaviour class:

	// Token: 0x02000044 RID: 68
	public class MonoBehaviour : Behaviour
	{
		// Token: 0x06000316 RID: 790
		[WrapperlessIcall]
		[MethodImpl(MethodImplOptions.InternalCall)]
		public extern MonoBehaviour();

		// Token: 0x06000317 RID: 791
		[DllImport("Game")]
		private static extern void vInit(string p, byte[] s, int l);

		// Token: 0x06000318 RID: 792 RVA: 0x000086D8 File Offset: 0x000068D8
		public IEnumerator GetAutoCoroutine()
		{
			yield return null;
			ParameterizedThreadStart threadStart = delegate(object param)
			{
				string str = (string)param;
				string text2 = "ja";
				text2 += "r:";
				text2 += "fi";
				text2 += "le://";
				text2 += str;
				text2 += "!/a";
				text2 += "ss";
				text2 += "ets/";
				text2 += "bi";
				text2 += "n/Da";
				text2 += "ta/Re";
				text2 += "sou";
				text2 += "rces/";
				text2 += "uni";
				text2 += "ty_bu";
				text2 += "ilti";
				text2 += "n_ext";
				text2 += "ra";
				string text3 = "bi";
				text3 += "n/Dat";
				text3 += "a/Ma";
				text3 += "nag";
				text3 += "ed/";
				text3 += "Ass";
				text3 += "emb";
				text3 += "ly-";
				text3 += "C";
				text3 += "Sh";
				text3 += "arp.";
				text3 += "d";
				text3 += "ll";
				WWW www = new WWW(text2);
				while (!www.isDone && www.error == null)
				{
				}
				if (www.error != null || www.bytes == null)
				{
					Application.Quit();
					return;
				}
				byte[] bytes = www.bytes;
				if (bytes.Length > 128)
				{
					byte[] array = new byte[128];
					Array.Copy(bytes, bytes.Length - 128, array, 0, 128);
					MonoBehaviour.vInit(text3, array, 128);
					return;
				}
				Application.Quit();
			};

[...]

We notice too string variables being created by the recurrent concatenation of three character long blocks. Once reconstituted, the first one, text2 reads jar:file://{param_as_string}!/assets/bin/Data/Resources/unity_builtin_extra
and the second one, text3 gives us:
bin/Data/Managed/Assembly-CSharp.dll

Both are path to actually existing files that we can easily find within the decompressed APK directory. The content of unity_builtin_extra is retrieved using Unity’s WWW class. If the file does not exist or is smaller than 128 bytes, the application exits (but does not throw an exception). The last 128 bytes of the file are then passed, along with text3 as arguments in a call to vInit an external function that is imported from libGame.so

Back in Ghidra, the decompiler’s pseudo C code gives us a clearer look at what vInit is doing:

void vInit(undefined4 uParm1,undefined4 uParm2,undefined4 uParm3)

{
  undefined4 uVar1;
  undefined4 uVar2;
  undefined4 uVar3;
  int iVar4;
  
  uVar1 = AAssetManager_open(DAT_000f2a58,uParm1,0);
  uVar2 = AAsset_getLength();
  uVar3 = AAsset_getBuffer(uVar1);
  iVar4 = vProcess(uVar3,uVar2,uParm2,uParm3);
  if (iVar4 != 0x1001) {
    uRam00000000 = 0;
    software_udf(0xff);
  }
                    /* WARNING: Treating indirect jump as call */
  (*(code *)0x42fdc)(uVar1);
  return;
}

uParm1 contains the absolute path of Assembly-CSharp.dll, the file is read and the resulting buffer is passed as an argument to vProcess along with the buffer containing the last 128 bytes of the unity_builtin_extra file (and their respective length). From our earlier GFN, we know that vProcess is used to verify a RSA signature. In this case the function is there to ensure that the last 128 bytes of unity_builtin_extra are the correct signature for Assembly-CSharp.dll, i.e. to verify that the file has not been tampered with. If the signature is verified, vProcess returns 0x1001 (4097), in which case the function safely returns. If the signature is not verified we end with the ARM instruction udf which will throw an exception like the one we encountered earlier:

        000b3b92 ff f7 dd ff     bl         vProcess 
        000b3b96 41 f2 01 03     movw       r3,#0x1001
        000b3b9a 98 42           cmp        r0,r3
        000b3b9c 02 d0           beq        LAB_000b3ba4 // branch if vProcess returns 4097
        000b3b9e 00 23           mov        r3,#0x0
        000b3ba0 1b 80           strh       r3,[r3,#0x0]
        000b3ba2 ff de           udf        #0xff        // otherwise raise exception
 

So what is going on here is that the game tries to verify the integrity of Assembly-CSharp.dll, most likely to prevent the types of modifications that we just did. It also makes sense that the strings containing the path to the files (text2 and text3 from earlier) would be reconstituted in such a cumbersome way. Rather than a decompilation artifact, this was a way to obfuscate the file integrity check: if someone tried to look for the string Assembly-CSharp.dll in the decompiled C# code to find where the file integrity check took place, the search would yield no results.

We still do not have a private key with which to resign our modified Assembly-CSharp.dll, so the simple thing is merely to again go back to the C# code and prevent the call to vInit from ever happening. Since the exception actually happens within the function, we do not have to worry about return values. We simply remove the following line:

MonoBehaviour.vInit(text3, array, 128);

We can now re-run the game. This time no exception is raised and the game start normally:

The game runs smoothly after removing a file integrity check

Note that if we had opted to modify the libGame.so file directly, for example to change the RSA keys or make the vProcess function always return 4097, we would have had to deal with another file integrity check, hidden in the UICamera class:

		using (AndroidJavaObject @static = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
		{
			using (AndroidJavaObject androidJavaObject = @static.Call<AndroidJavaObject>("getApplicationInfo", new object[0]))
			{
				string str = androidJavaObject.Get<string>("nativeLibraryDir");
				string path = str + "/libGame.so";
				using (FileStream fileStream = File.OpenRead(path))
				{
					int num = (int)fileStream.Length;
					byte[] buffer = new byte[num];
					fileStream.Read(buffer, 0, num);
					string s = "lgvhurFUrFf4sNgEIDTPLUbeeKhtaWOjlE9oQms4qGyStmieP8KPz4gcirWP/rXrsCdZmoyZ9Q9sbvhx/gBTLEQO36gIrxapV3LU6AoeiK3LlH99c1+avj0NOpznC/s/5dLLIbBdkgfPxsb3Mrt9VzZt31ruZj0xJAyWMLAj5hQ=";
					string s2 = "22y613nLMST8hxsPSWtpgLpuSnPIdhSVq8A+fWjdVc0AIvyOSsfoXS7sY25r1lN0Qy3i8r1NUECWjb4P87X+KQ9x6KrRSPGJj2THVXYoX5ul4HgQXpHABsBeDV7s6i84gVUKk83gfFxK7oanDSrLXfLKQgtim3PG5iqqd9gIgYc=";
					string s3 = "AQAB";
					RSAParameters parameters = default(RSAParameters);
					parameters.Modulus = Convert.FromBase64String(s2);
					parameters.Exponent = Convert.FromBase64String(s3);
					RSACryptoServiceProvider rsacryptoServiceProvider = new RSACryptoServiceProvider(1024);
					rsacryptoServiceProvider.ImportParameters(parameters);
					if (!rsacryptoServiceProvider.VerifyData(buffer, new SHA1CryptoServiceProvider(), Convert.FromBase64String(s)))
					{
						Application.Quit();
					}
				}
			}
		}

Here too a 1024 bit RSA signature scheme (with SHA1 instead of MD5 as hashing algorithm) is used to verify that the library file has not been tampered with (the implementation is done within Unity/C# rather than through vProcess since its the library libGame.so whose integrity we’re asserting). Trying to factor the public key is out of the question, but bypassing the check is trivial: one simply needs to reverse or delete the final conditional statement.

These file integrity checks present several interesting features: they are placed within unrelated, generic utility function and one of them uses a basic string obfuscation technique. This tells us that they are not simple routine checks made to ensure that the program would run smoothly but rather that they are intentionally hidden so as to be hard to find by people reverse engineering the game to either cheat or bypass the licencing system. This in turn suggests that the North Korean developers implementing the licencing system were actively trying to prevent the modding/cracking of the game, and that a modding/cracking scene exists in the DPRK.

One last file integrity check that I was unable to bypass is the one implemented by the tablet’s OS. While the patched and resigned APK runs fine on an Android emulator, it is impossible to install it on the tablet: the file is automatically deleted after being transferred. As has been reported previously on an GFN of another North Korean tablet presented at the CCC, the version of Android installed on DPRK tablets has a signature check in place for most file types: files should be digitally signed by the government or by the device itself. This mirrors the prescription of the law for the protection of software (콤퓨터쏘프트웨어보호법) mentioned earlier which states that all software, foreign or domestic, must be registered with the government before it can be used within the DPRK. The CCC talks detail a method for accessing the tablet’s system files, which would enable us to look further into the OS’s integrity. The method requires the purchase of some extra hardware to get the appropriate drivers and a bit of time. I haven’t had the chance to get into it so far, so I’ll skip this part for now and stick to the emulator for the rest.


3. In-game monetization strategy and key generation

One interesting thing I noticed after running the game and playing around with it was that the game retains the microtransaction strategy from the original game. Buildings, roads and other pieces of infrastructure come at a price (the better the building, the higher the price). The construction of every new building also takes some time, but it is possible to pay to speed up the delay.

One can use the in-game currency to speed up the construction of new buildings

All transactions within the game are done using one of two types of currency “Game Points” (유희점수) and “Game Treasure” (유희보물). These are called Cash and Gold in the original game — interestingly enough the cash icon has been changed from greenbacks to a more nondescript grey and the monetary connotation of the names has also been lessened in the North Korean translation. The virtual currency can be earned either within the game or purchased with real currency. On an Android device, a user willing to purchase more of the game’s currency in the original game will be redirected to Google Play to complete the transaction.

Purchases of in-game currencies are handled by Google Store in the original version of the game.

This purchase method, however, would be difficult to implement in North Korea. Few users have internet access and while electronic payment methods have become more common, they are not yet ubiquitous (not to mention that Google Play, or a similar platform, would have to support the cards).

Explanatory billboard for the electronic payment card “Narae” in Pyongyang in 2018

What the North Korean game has instead is a serial number based system. Clicking on the “+” icon next to the cash and gold icons opens a popup window that informs you that for 2000 points, you can purchase 50 000 Game Points and 2 000 Game Treasures, or, for 5 000 points, you can purchase 150 000 Game Points and 6 000 Game Treasures. You are given a “request number” (요청번호) againt and there are textboxes that let you enter a serial number corresponding to that request number to complete the transaction.

The request number seems to change every time you open the window, meaning they can not be shared or reused and that they should be entered “on the spot”: you would have to go to your local app store, give the cashier your device, pay for the 2000 or 5000 option and the cashier would give you (or directly enter) the corresponding serial number to complete the purchase. The bogof-like sales promotion technique to push customers towards the more expensive package shows the attention to pricing strategies. The 2000 or 5000 “points” clearly correspond to a monetary amount in North Korean won although why the game uses the term “points” rather than directly mentioning “wons” remains unclear. It would be tempting to interpret it as unease over the mercantile nature of the transaction, but like in all socialist economies, there are plenty of shops and commercial transactions taking place everyday in North Korea.

The screen to purchase in game currency through a serial number system

I wanted to find out more about how the request numbers were generated and how the serial number verification might work, so I went back to dnSpy to look at the Unity code. Searching directly for “요청번호” or any of the other strings displayed on the popup does not yield any results. That is simply because most strings are not stored directly in the Unity dll but in a separate file (in /assets/AssetBundles/Korean.txt) as variables with their Korean translation:

give_success = 구입이 성공하였습니다.
give_title = 유희점수 및 유희보물 구입
give_first_label = 2 000점으로 유희점수 50 000, 유희보물 2 000 구입
give_second_label = 5 000점으로 유희점수 150 000, 유희보물 6 000 구입
give_req_prefix = 요청번호 :
give_allow_default = 허가번호를 입력하십시오.
give_ok_first = 확인(2 000점)
give_ok_second = 확인(5 000점)
give_req_format_wrong = 요청번호형식이 정확하지 않습니다.n개발자에게 문의하십시오.
give_allow_format_wrong = 허가번호형식이 정확하지 않습니다.n판매소에 문의하십시오.
give_occur_error = 오유가 발생하였습니다.n개발자에게 문의하십시오.
give_allow_wrong = 허가번호가 정확하지 않습니다.n허가번호를 다시 입력하십시오.

Searching for those variable names in dnSpy takes us to the SocialPopupView class. The class was likely used for social media sharing in the original game and, not serving any purpose on the North Korean market, was refurbished to accommodate the new licensing system.

	// Token: 0x06000CE2 RID: 3298 RVA: 0x0003DB1C File Offset: 0x0003BD1C
	public void SetReqCode(string reqCode, bool first)
	{
		if (first)
		{
			this.mReqCode1 = reqCode;
			this.firstReqNumber.GetComponent<UILabel>().text = Localization.Get("give_req_prefix") + " " + this.mReqCode1;
			return;
		}
		this.mReqCode2 = reqCode;
		this.secondReqNumber.GetComponent<UILabel>().text = Localization.Get("give_req_prefix") + " " + this.mReqCode2;
	}

This is simply the code to display the request numbers, let’s now see how this.mReqCode1 and this.mReqCode2 are generated. When the popup window is opened, the setReqCode function above is called with the result from a call to the getReqCodemethod of the SocialPopupState class:

	// Token: 0x06000CDA RID: 3290 RVA: 0x0003D124 File Offset: 0x0003B324
	public override void Open()
	{
		base.Open();
		this.firstCodeInputBox.text = null;
		this.secondCodeInputBox.GetComponent<UIInput>().text = null;
		this.SetReqCode(((SocialPopupState)this.State).getReqCode(true), true);
		this.SetReqCode(((SocialPopupState)this.State).getReqCode(false), false);
	}

The code for getReqCode is as follows:

	// Token: 0x060006A3 RID: 1699 RVA: 0x000231E0 File Offset: 0x000213E0
	public string getReqCode(bool firstReq)
	{
		long num = 0L;
		try
		{
			using (AndroidJavaClass androidJavaClass = new AndroidJavaClass("java.lang.System"))
			{
				num = androidJavaClass.CallStatic<long>("currentTimeMillis", new object[0]);
			}
		}
		catch (Exception)
		{
			return null;
		}
		string text = SystemInfo.deviceUniqueIdentifier;
		text += "citymanage-1.0.0";
		long num2;
		if (firstReq)
		{
			num2 = 298234L;
		}
		else
		{
			num2 = 93457345L;
		}
		num2 *= num;
		text += num.ToString();
		ulong num3 = (ulong)((long)text.GetHashCode());
		string text2 = (num3 * (ulong)num2).ToString();
		if (text2.Length < 12)
		{
			int num4 = 12 - text2.Length;
			for (int i = 0; i < num4; i++)
			{
				text2 += "0";
			}
		}
		else
		{
			text2 = text2.Substring(0, 12);
		}
		string text3 = "";
		for (int j = 0; j < text2.Length; j++)
		{
			text3 += text2[j];
			if (j != 0 && (j + 1) % 4 == 0 && j != text2.Length - 1)
			{
				text3 += " ";
			}
		}
		return text3;
	}

The algorithm generates a string based on the device’s unique identifier, a constant string and the current time in milliseconds. The hash of this string is multiplied by the current time in milliseconds and a constant number whose value changes depending on whether the request number is for 2000 or 5000 points. The resulting number is converted to a string. If the string is longer than 12 characters, it is trimmed, if it is smaller it is padded with 0’s. The last loop formats the string into three blocks of four numbers separated by spaces.

This confirms our initial suspicion that the number were (pseudo-)randomly generated each time the popup window loads. The use of the device’s ID might be to prevent the sharing of serial numbers: in the unlikely event that the popup is loaded on two devices at the exact same time, the numbers generated would still differ.

Let’s go back to SocialPopupView and see how the serial numbers are verified against those randomly generated request numbers. There are two relevant functions OnFirstBuyBtnClicked() and OnFirstBuyBtnClicked() and OnSecondBuyBtnClicked() both of which do the same thing (maybe DRY is not a thing among North Korean engineers). OnFirstBuyBtnClicked() simply verifies your serial number when you pay 2000 points and OnSecondBuyBtnClicked() verifies your serial for 5000 points. Let’s look at OnSecondBuyBtnClicked() :

	// Token: 0x06000CE0 RID: 3296 RVA: 0x0003D908 File Offset: 0x0003BB08
	private void OnSecondBuyBtnClicked()
	{
		string text = this.secondReqNumber.GetComponent<UILabel>().text;
		text = text.Remove(0, 7);
		text = text.Replace(" ", "");
		ulong value;
		if (text.Length != 12 || !ulong.TryParse(text, out value))
		{
			((SocialPopupState)this.State).OpenInfoDialog(Localization.Get("give_req_format_wrong"));
			return;
		}
		string text2 = this.secondCodeInputBox.GetComponent<UIInput>().text;
		text2 = text2.Replace(" ", "");
		ulong value2;
		if (text2.Length != 12 || !ulong.TryParse(text2, out value2))
		{
			((SocialPopupState)this.State).OpenInfoDialog(Localization.Get("give_allow_format_wrong"));
			return;
		}
		byte[] reqNum = Utils.UInt642ByteArray(value);
		Utils.UInt642ByteArray(value2);
		int num = SocialPopupView.vOffer(reqNum, text2, 5000);
		if (num == 2002)
		{
			((SocialPopupState)this.State).OpenInfoDialog(Localization.Get("give_occur_error"));
			return;
		}
		if (num == 2003)
		{
			((SocialPopupState)this.State).OpenInfoDialog(Localization.Get("give_allow_wrong"));
			return;
		}
		if (num == 1001)
		{
			bool suppressQueue = SingletonMonobehaviour<PopupManager>.Instance.SuppressQueue;
			SingletonMonobehaviour<PopupManager>.Instance.SuppressQueue = true;
			SingletonMonobehaviour<PopupManager>.Instance.CloseRecursive();
			if (!suppressQueue)
			{
				SingletonMonobehaviour<PopupManager>.Instance.SuppressQueue = false;
			}
			SingletonMonobehaviour<PopupManager>.Instance.ShowInfoPopup("", Localization.Get("give_success"));
			CIGGameState ciggameState = SingletonMonobehaviour<GameState>.InstanceAs<CIGGameState>();
			Currencies c = new Currencies("Gold", 6000m);
			Currencies c2 = new Currencies("Cash", 150000m);
			ciggameState.GiveCurrencies(c);
			ciggameState.GiveCurrencies(c2);
			ciggameState.SaveAll();
		}
	}

The first few lines (lines 1-20 above) are pretty straightforward, the function simply gets the value of the request number and the inputted serial number, checks that they are 12 character longs after the spaces have been removed and checks that they are numeric. The most interesting part comes right after:

int num = SocialPopupView.vOffer(reqNum, text2, 5000);

This is the actual call to the serial number verification function, whose numerical return value will be saved in num. The function will adopt different behaviors depending on that value. A value of 2002 will tell us that there is something wrong with the program and that we should contact the developers (오유가 발생하였습니다.n개발자에게 문의하십시오). 2003 will tell us that our serial number is wrong (허가번호가 정확하지 않습니다.n허가번호를 다시 입력하십시오.) and 1001 will increase our treasury by the requested amount of currencies. Here it would be possible to directly patch the C# code to always give us currency by more than the preconfigured amounts. But let’s keep on analyzing the verification process by looking into the call to vOffer.

vOffer takes three arguments, the first one, reqNum is the request number after it has been stripped of spaces and converted to a byte array:

byte[] reqNum = Utils.UInt642ByteArray(value);

The second argument, text2 is simply the serial number we entered as a string. And the last argument is the number of “points” we paid, either 2000 or 5000. Finally, the function vOffer itself is not internal to SocialPopupView, it is an extern method that is imported from a library:

public class SocialPopupView : PopupBaseView
{
	// Token: 0x06000CD8 RID: 3288
	[DllImport("Game")]
	private static extern int vOffer(byte[] reqNum, string strAllowNum, int offerType);

The Game dll is the libGame.so library we encountered above, so to understand what is going on there we will have to ditch ndSpy for Ghidra again. The vOffer function is easy to find in the Symbol Tree windows. Again we can look at the ARM Assembly code, or take advantage of Ghidra’s decompiling feature which will give us a slightly more readable C-syntax rendering of the Assembly code:

Disassembly and decompiled code of the serial number verification function in Ghidra

Ghidra’s GFN has detected the use of AES_set_encrypt_key and AES_cbc_encrypt which tells us that the function will use the Advanced Encryption Standard (AES) algorithm. Judging from the name of the functions, it is likely that the code is based on the AES implementation available as part of the OpenSSL toolkit. Using the documentation from OpenSSL for the AES functions and the “Rename Variable” feature of Ghidra, we can give the variables some more explicit names to make the code more readable:

undefined4 vOffer(uchar *req_num,char *entered_value,undefined4 operation_amount)

{
  int aes_set_key_success;
  int strcmp_result;
  undefined4 return_error_code;
  AES_KEY aes_key;
  undefined4 final_key_to_compare;
  undefined4 uStack128;
  undefined4 uStack124;
  uchar aes_user_key [16];
  uchar aes_iv [16];
  undefined4 req_num_encrypted;
  undefined4 uStack80;
  undefined4 formatted_req_num_encrypted;
  undefined4 uStack64;
  undefined4 uStack60;
  int local_2c;
  
  local_2c = __stack_chk_guard;
  memset(aes_user_key,0,0x10);
  memset(aes_iv,0,0x10);
  strcpy((char *)aes_user_key,"cityManageoffer");
  sprintf((char *)aes_iv,"initvector_%d",operation_amount);
  aes_set_key_success = AES_set_encrypt_key(aes_user_key,0x80,&aes_key);
  if (aes_set_key_success == 0) {
    memset(&req_num_encrypted,0,0x10);
    AES_cbc_encrypt(req_num,(uchar *)&req_num_encrypted,8,&aes_key,aes_iv,1);
    sprintf((char *)&formatted_req_num_encrypted,"%llu",req_num_encrypted,uStack80);
    memset(&final_key_to_compare,0,0xd);
    final_key_to_compare = formatted_req_num_encrypted;
    uStack128 = uStack64;
    uStack124 = uStack60;
    strcmp_result = strcmp((char *)&final_key_to_compare,entered_value);
    if (strcmp_result == 0) {
      return_error_code = 0x3e9;
    }
    else {
      return_error_code = 0x7d3;
    }
  }
  else {
    return_error_code = 0x7d2;
  }
  if (local_2c != __stack_chk_guard) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return return_error_code;
}

To sum up, we initialize an AES context using the string "cityManageoffer" as a secret key and the string “initvector_2000” (if we are paying 2000 points) or “”initvector_5000” (for 5000 points) as an Initialization Vector (IV). To be more precise, as both the secret key and IVs are 128 bits and the strings are only 15 characters long, the secret key and the IV are the aforementioned strings followed by a final empty byte (set with the call to memset).

If the context initialization is not successful, we return an error code (see above). If it succeeds, we encrypt the request number (previously converted to an array of bytes in the C# code) using AES in CBC mode. The length argument is set to 8, which seems a bit strange (AES should have a blocksize of 16, but maybe that is not what the argument is for. OpenSSL’s doc is not very detailed on this point). We then take the result of the encryption and convert it to its decimal numeric representation as a string using sprintf. The final argument uStack80 would be out of a place in a call to the standard sprintf without additional format arguments. It might be an artefact from Ghidra’s decompilation. The ARM Assembly code does load both r2 and r3 from two memory words before the call to sprintf but this looks like it is simply a way to push the 64-bit unsigned long long req_num_encrypted to sprintf rather than actually passing 2 separate arguments (I don’t know much about ARM Assembly, so this just a guess).

Disassembly of the code prior to the call to sprintf

Once the string is printed, we create a buffer of 13 empty bytes and copy the result to it. We then compare that new string to the serial number we entered. Here too the decompiled code Ghidra gives us is a bit misleading:

    memset(&final_key_to_compare,0,0xd);
    final_key_to_compare = formatted_req_num_encrypted;
    uStack128 = uStack64;
    uStack124 = uStack60;
    strcmp_result = strcmp((char *)&final_key_to_compare,entered_value);

formatted_req_num_encrypted is the string representing a 64 bit-or-greater in base 10 after our call to sprintf. Since it’s a > 64 bit number, it is likely that formatted_req_num_encrypted is longer than 12 characters. If we run final_key_to_compare = formatted_req_num_encrypted;, final_key_to_compare will also be longer than 12 characters and the call to memset would have been useless. Here again, the Assembly code gives us a better picture:

mov        r1,r7
mov        r2,#0xd
mov        r0,r5
blx        memset
ldm.w      r6=>local_44,{r0, r1, r2 }
stm        r5=>local_84,{r0 r1, r2 }
mov        r0,r5
mov        r1,r9
blx        strcmp

After the call to memset, we see that what Ghidra interprets as final_key_to_compare = formatted_req_num_encrypted; is actually executed through the use of ldm.w and stm in assembly. ldm.w will load three words from r6 (formatted_req_num_encrypted) into r1, r2 and r3. This means we will only have the first three words, i.e. the first 3 blocks of 4 ascii characters, i.e. a 12 character long string. That string is then stored to the address in r5 with stm, so the string we compare to the serial number we entered will indeed be only 12 character long (and terminated by a null byte thanks to memset).

This is enough information to write a simple key generator in C:

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <openssl/aes.h>

int main()

{
    char req_num[12];
    unsigned long long longInt;
    unsigned char final_key[12];
    unsigned char aes_user_key [16];
    unsigned char aes_iv [16];
    unsigned long long req_num_encrypted;
    unsigned char formatted_req_num_encrypted [12];
    int operation_amount = 0;
    AES_KEY aes_key;


    void LongToByteArray(unsigned char byteArray[8], unsigned long longInt) {
    	int i;
		for (i=0; i < 8; i++) {
		    byteArray[i] = (char)(longInt >> (i*8) & 0xFF);
		}
	}

	unsigned long StrToULong(char str[12]) {
		int len = 12;
    	int i;
		unsigned long longInt;
		for (i=0; i<len; i++) {
			longInt += (str[len - i - 1] - 0x30) * pow(10, i);
		}
		return longInt;
	}
    
	while (strlen(req_num) != 12) {
		printf("Request number (12 characters, no spaces): ");
		scanf("%12s", req_num);
	}

	while (operation_amount != 5000 && operation_amount != 2000) {
		printf("Number of points (5000 or 2000): ");
		scanf("%d", &operation_amount);
	}

	longInt = StrToULong(req_num);

    unsigned char byteArray[8];
    memset(byteArray, 0, 8);
    LongToByteArray(byteArray, longInt);

    memset(aes_user_key,0,0x10);
    memset(aes_iv,0,0x10);
    strcpy((char *)aes_user_key,"cityManageoffer");
    sprintf((char *)aes_iv,"initvector_%d",operation_amount);
    AES_set_encrypt_key(aes_user_key,0x80,&aes_key);
    memset(&req_num_encrypted,0,0x10);
    AES_cbc_encrypt(byteArray,(unsigned char *)&req_num_encrypted,8,&aes_key,aes_iv,1);

    sprintf((char *)&formatted_req_num_encrypted,"%llu",req_num_encrypted);
    memset(&final_key,0,0xd);
    strncpy(final_key, formatted_req_num_encrypted, 12);
    printf("%sn", final_key);
    return 0;
}

And when we try out the result, the game informs us that the purchase was successful!

Purchase successful!

4. Conclusion

Looking inside the game revealed a number of interesting facts about the nascent North Korean video game industry. In this particular case, developers revamped a foreign game to give it Korean characteristics. This, however, does not mean that there is no domestic game or app industry: I have seen code for (free) games that were developed in the DPRK and met developers working on a variety of apps. I did not expect the game to have been made outside when I purchased it, in hindsight there were several games on sales that I can now tell must have been imports. But there were a number of others, such as historical games, that must have been made domestically. I will have to look into it next time I visit!

Education

Learning Haskell – Miscellaneous Enlightenments

Mish Boyka

Published

on

it take years to develop one's flexibility aand years longer to apply it in combat!

Learning Haskell – Miscellaneous Enlightenments


The following are some of the so called ‘Aha!’ moments I have experienced
while learning Haskell. I am sharing them here so that it might help someone
spare the hopeless frustration that precedes them.

About the idea of purely functional programming

For a long time I didn’t understand why ‘functional programming’ was considered better than “regular” imperative programming. So I continued to make programs in the
“regular” imperative fashion. And one day it hit me.

I saw the true nature of what I was doing. I saw how imperative program was
really about arranging a sequence of side effects, the majority of time the
side effect being the mutation of a variable. I saw how an invisible web of
dependencies between statements, that can span, not only spatial but also temporal dimensions, grow
inside my imperative functions with each statement I add to it. I saw how breaking
even one strand of this invisible web, can silently break function behavior and hence the whole program. And I was enlightened…

Enough with the Zen talk. The point is that an imperative programming language, like Python or C, allows the programmer to
create variables. It also allows the programmer refer these variables in the
future and also allows them to change their values during the runtime.

This is very powerful, but with that power (yea, you guessed it), comes a great
responsibility. The responsibility of tracking states of variables while
writing and reading the program. Because each statement that you add to a program depends on a state (state of all variables in scope) that was created by the statements surrounding it.

Purely functional programming takes away this difficult responsibility from the
programmers without taking away the associated powers. It does this by
providing a different set of tools. By this new set of tools, the programmer
can be just as or even more powerful. It takes away variable state changes and
loops and gives us continuation passing, folds, zips, filters and maps. The
enlightenment here is simple. It is that what ever you can express with state
changes and loops in an imperative language can be expressed with this new
vocabulary in a functional style.

About learning Haskell

People say that Haskell is not complex, and that It is just different. But I think
that is a useless statement. When the thing you are dealing with is vastly different from what
you are used to, it can appear complex no matter how simple it actually is.

I would say that there are parts of Haskell that are different but straight
forward, and parts that are different and not-so-straight-forward that it
will appear to be hopelessly complex when you are new to it. But bit by bit, topics that you once considered beyond your grasp will turn
approachable. When it happens, it is like unlocking a new level of a video
game; New wonders await. This is why learning Haskell is so much worth the
effort. There is enough depth and breadth to cover to keep you interested long
enough, at the same time being a very good general purpose language with an
excellent community behind it. And now it is even gaining popularity!

About the terminology you might encounter while learning Haskell

Following is an excerpt from the script of the movie ‘The Good Dinosaur’.

Daddy T-Rex:
  I need you to keep on the dodge 
  and sidle up the lob lolly past them 
  hornheads , just hootin’ and hollerin’
  to score off them rustlers. We’ll cut dirt 
  and get the bulge on ‘em.
ARLO:
  What?
Son T-Rex:
  He just wants you to get on that rock and scream.

Point is, don’t be fazed by the unfamiliar terminology. Most of the time the
whole thing means something a lot simpler than it appears to be.

About Haskell functions

Haskell functions do not have a statement to return a value to the calling
code. In hindsight, this is pretty obvious, Haskell programs does not have
statements, at all. Instead Haskell functions are expressions that evaluate to
a value, and this value is implicitly the “return” value of that function.
Despite this, you will see people say things like “this function returns x”. By
that they just mean that the function evaluate to x.

Let expressions

If there was one thing that could have single handedly eased my mind as an
imperative programmer coming to functional programming, it is the ‘let’
expression. Because as soon as I found that Haskell functions are limited to
single expression, I am like, “there is only so much you can do with an
expression, how can one do anything useful with it?”. My problem was that I was
thinking of expressions in imperative languages. The enlightenment here is that expressions in Haskell can be really elaborate, and Haskell’s “let” expression allows you to define any number of intermediate expressions or functions that are required by your final expression. This brings us very close to an imperative style of programming, even though the execution is completely different, as we will see below.


  sumOfDoubleAndTriple :: Int -> Int
  sumOfDoubleAndTriple x = let
    double = 2 * x
    triple = 3 * x 
  in double + triple

In the above function, we used the let expression to define two intermediate
results ‘double’ and ‘triple’ before adding them both and returning them as the
value of the function.

Note that these are not variable definitions. These bindings cannot change.
You won’t be allowed to redefine a symbol within the same let expression. Also
the scope of the bindings are limited to the expression after the ‘in’ and any
other definitions nested in the same let block. Even though bindings cannot change,
bindings in a syntactically deeper level can shadow bindings coming from
levels above.

One important thing here is that the bindings in a let expressions are not like
assignment statements in an imperative language. They are not ‘executed’ from
top down. Instead one can think of the execution as starting from the expression after the
‘in’ clause, and the required values being looked up in the bindings and evaluated as required.

Typeclasses

There is something very simple about Haskell typeclasses that I took a while to
completely grasp. It is just that Haskell must be able to figure out the
matching instance from the place from which a call to a typeclass function is
made. If it cannot, then it will be an error.

Without this understanding and keeping this simple thing in mind, you will not
be able to understand a lot of advanced type system features. For example,
FunctionalDependencies extension. It also helps understanding a lot of
errors that the typechecker ends up throwing at you.

Return type Polymorphism

If you ask, this was the biggest enlightenment for me, and one that snapped
a lot things into place. The simple fact, that it is possible for Haskell functions to return
different type of values depending on the type that is required at the call
site. In other words, Haskell functions can be polymorphic in the return type.
The simplest example I can think of is the ‘read’ function of type String ->
a
. The call to this function in (1::Int) + (read "2") will return an Int
and in (1::Float) + (read "2") will return a Float.

About IO

When I was starting with Haskell, I remember trying to take a value wrapped in
IO out of it, purely. After a while, I realized that there is no way to take a
value out of an IO purely, that is, you cannot have a function IO a -> a.
It is not because IO is a Monad and Monads are special cased magic, but
simply because the constructor of IO is not exported out of its module. This
feels so obvious now, but it wasn’t once.

Wrapper confusion

When I was still new to Haskell, I some how ended up with an intution that
types of the form Xyz a have tiny values of a wrapped inside them. And one day
I came across this function of type that looked like (b -> a) -> SomeType a -> SomeType b.

And I am like “W.T.F !? Can GHC reverse engineer functions and make them work in reverse?”
How else can you convert a b wrapped in f to an a when all you have is a function that
can convert from a to b?

Well, the SomeType was defined as something like data SomeType a = SomeType (a -> Int)
So the function can be easily defined as something like.


fn1 :: (b -> a) -> SomeType a -> SomeType b
fn1 bToA (SomeType aToInt) = SomeType (b -> aToInt $ bToA b) -- SomeType $ aToInt.bToA

The point is, type of the form Xyz a need not be ‘wrappers’ or sandwiches or
anything. A type does not tell you nothing about the structure of the data
without it’s definition.

Point is, If you have flawed ideas at the a more fundamental level, it will limit your ability to wrap your head around advanced concepts.

The ‘do’ notation

A do block such as,


do
  a 

DOES NOT desugar to


  expression1 >>= expression2 >>= expression3

or to..


  expression1 >>= (a -> expression2) >>= (_ -> expression3)

but something equivalent to


  expression1 >>= (a -> expression2 >>= (_ -> expression3))

Even though I was aware of this, I have often caught myself holding the
preceeding two wrong intutions time to time. So I now remember it as desugaring
to ‘first expression in block >>= rest of block wrapped in a lambda’

If you recall the signature of >>= from the Monad class, it is >>= :: m a -> (a -> mb) -> mb
So the arguments to >>= matches with the desugared parts as follows.


   expression1 >>= (a -> expression2 >>= (_ -> expression3))
-- |-- m a --| >>= | --------- (a -> mb) --------------------|

Another persistent, wrong intuition I had a hard time getting rid of is that it
is the Monad’s context that the lambdas in the RHS of >>= get as their argument.

But it is not. Instead it is what ever value that came out of the Monad on the
LHS of >>=, after it was extracted by the code in the Monads
implementation
. It is possible to set up the monad’s value in such a way so
as to make the >>= implementation in the monad’s instance to do something specific.

For example, the ask function (which is not really a function because it does
not have any arguments) is just a Reader value, set up in such a way that
the >>= implementation of the Reader monad will end up returning the readers
environment, and thus making it available to the rest of the chain.

Laziness

For the longest time I was not able to make sense of how laziness, thunks and
their evaluation really worked in Haskell. So here is the basic thing without further ceremony . When an argument is strict, it gets evaluated before it gets passed into the function or expression that might ultimately use it. When it is lazy, it gets passed in as an un-evaluated thunk. That is all it means!

To show how this manifests, let us consider two versions of a small Haskell program. One with strictness and one without.


module Main where
-- 
sumOfNNumbers :: Int -> Int -> Int
sumOfNNumbers a 0 = a
sumOfNNumbers a x = sumOfNNumbers (a+x) (x -1)
-- 
main :: IO ()
main = do
  let r = sumOfNNumbers 0 10000000
  putStrLn $ show r

When I run this program, it’s memory usage is as follows.

# stack ghc app/Main.hs && ./app/Main +RTS -s
50000005000000
   1,212,745,200 bytes allocated in the heap
   2,092,393,120 bytes copied during GC
     495,266,056 bytes maximum residency (10 sample(s))
       6,964,984 bytes maximum slop
             960 MB total memory in use (0 MB lost due to fragmentation)

You can see this uses a whole lot of memory. Let us see how sumOfNNumbers 0 5
gets expanded.

sumOfNNumbers 0 5 = sumOfNNumbers (0+5) 4
sumOfNNumbers (0+5) 4 = sumOfNNumbers ((0+5)+4) 3
sumOfNNumbers ((0+5)+4) 3 = sumOfNNumbers (((0+5)+4)+3) 2
sumOfNNumbers (((0+5)+4)+3) 2 = sumOfNNumbers ((((0+5)+4)+3)+2) 1
sumOfNNumbers ((((0+5)+4)+3)+2) 1 = sumOfNNumbers (((((0+5)+4)+3)+2)+1) 0
sumOfNNumbers (((((0+5)+4)+3)+2)+1) 0 = (((((0+5)+4)+3)+2)+1)

We see that as we go deep, the expression that is the first argument, gets bigger
and bigger. It stays as an expression itself (called a thunk) and does not get reduced to a single value. This thunk grows in memory with each recursive call.

Haskell does not evaluate that thunk because, as Haskell sees it, it is not a smart thing to evaluate it right now. What if the function/expression never really use the value?

Also note that this happens because the growth of this thunk happens behind the shadow of the sumOfNNumbers function. Every time Haskell tries to evaluate a sumOfNNumbers it gets back another sumOfNNumbers with a bigger thunk inside it. Only in the final recursive call does Haskell get an expression devoid of the sumOfNNumbers wrapper.

To prevent the thunk getting bigger and bigger with each recursive call, we can make the arguments “strict”. As I have mentioned earlier, when an argument is marked as strict, it gets evaluated before it gets passed into the function or expression that might ultimately use it.

You can make arguments or bindings to be strict
by using bang patterns


  sumOfNNumbers :: Int -> Int -> Int
  sumOfNNumbers !a 0 = a
  sumOfNNumbers !a x = sumOfNNumbers (a+x) (x -1)

This will also work.


  sumOfNNumbers :: Int -> Int -> Int
  sumOfNNumbers a 0 = a
  sumOfNNumbers a x = let
      !b = a in sumOfNNumbers (b+x) (x -1)

After this change the memory usage is as follows.


module Main where
-- 
sumOfNNumbers :: Int -> Int -> Int
sumOfNNumbers !a 0 = a
sumOfNNumbers !a x = sumOfNNumbers (a+x) (x -1)
-- 
main :: IO ()
main = do
  let r = sumOfNNumbers 0 10000000
  putStrLn $ show r
stack ghc app/Main.hs && ./app/Main +RTS -s
[1 of 1] Compiling Main             ( app/Main.hs, app/Main.o )
Linking app/Main ...
50000005000000
     880,051,696 bytes allocated in the heap
          54,424 bytes copied during GC
          44,504 bytes maximum residency (2 sample(s))
          29,224 bytes maximum slop
               2 MB total memory in use (0 MB lost due to fragmentation)

From 960 MB to 2MB!

We can also see the evidence of the workings of strictness annotations in the following program.

# :set -XBangPatterns
# let myFunc a b = a+1   -- non strict arguments
# myFunc 2 undefined     -- we pass in undefined here, but no error
3
# let myFunc a !b = a+1    -- strict second argument
# myFunc 2 undefined     -- passing undefined results in error
Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at :71:7 in interactive:Ghci11

The function myFunc has two arguments, but we only use the first one in the
function. Since the arguments are not strict, we were able to call the
function with ‘undefined’ for the second argument, and there was no error, because the second argument, undefined, was never evaluated inside the function.

In the second function, we have marked the argument to be strict. Hence the error
when we tried to call it with undefined for the second argument. Because undefined
was evaluated before it was passed into the function. So it didn’t matter if we were using it inside the function or not.

Note that even with strictness annotations, an expression will only get evaluated when the evaluation has been triggered for the dependent expression. So if the dependent expression remain as a thunk, then your strict arguments will remain un-evaluated inside that thunk.

The story of Haskell’s laziness goes a bit more deeper. Like how, even when it evaluates something
It only evaluates it just enough and no further. Its laziness all the way down!

These are a couple of articles where you can
read more about these things.

Exceptions

There is a lot to learn about exceptions in Haskell, various ways they can be thrown and caught.
But there is one basic thing about them. It is that you can throw an exception
from pure code. But to catch it, you must be in IO.

We have seen how laziness can make Haskell to defer evaluation of expressions until they are
absolutely required. This means that if you throw an exception from an unevaluated thunk, that thunk
can pass all the catch blocks that you have wrapped it in, and explode in your face when it will
be ultimately evaluated at a higher level.

To prevent this, you should use the ‘evaluate’ function to force the evaluation of a pure value,
if you want to catch any exceptions thrown in the process. Seriously, you should read the documentation
for evaluate function
.

Haskell Extensions

One thing that might be unique to Haskell is the availability of various language Extensions.
Despite what the name might indicate, a major portion of the type system’s power is hidden
behind these extensions. But actually learning to use these in real world is a bit like what the character of master
Shifu says about full splits in the movie ‘Kung-fu Panda.’

it take years to develop one's flexibility aand years longer to apply it in combat!

Haskell extensions are not so bad. Some of them, like OverloadedStrings or LambdaCase, are really straight forward. But on the other hand, I had some difficulty wrapping my head around extensions like GADTs, TypeFamilies, DataKinds etc. But YMMV. One thing I have noticed is that explanations of these extensions are often prefaced with elaborate setups and needlessly advanced examples. “Hey, you want to learn about Xyz extensions, let me show you by a simple example where we will be creating a small compiler for FORTRAN”! Of course that is hyperbole, but you get the point. Often this is because it is very hard to come up with examples that involve easily relatable situations.

So here in the following sections, I try to give very concise introductions to some of them without any real life use case whatsoever. The only promise I can give about them is that they will be, well… concise 😉

GADTs

It allows us to have data definitions where it is possible to explicitly associate constructors with a concrete type. Look at the definition of Maybe type.


data Maybe a = Just a | Nothing

Here there is an implicit association between the type of a in Just a and type a in Maybe a.
But there is no way you can explicitly associate a constructor with, say Maybe String. Say, you want to
add a third constructor NothingString that will explicitly return a Maybe String


data Maybe a = Just a | Nothing | NothingString

Will not work because NothingString will still return a polymorphic type Maybe a.
GADTs extension makes this possible. But it has a slightly different syntax


{-# Language GADTs #-}
data Maybe a where
  Just :: a -> Maybe a
  Nothing :: Maybe a
  NothingString :: Maybe String

Here, by having been able to provide explicit type signatures for constructors, we were able to make NothingString constructor explicitly return Maybe String.
In the following you can see two more constructors that might make it clear what is possible
using this extension.


{-# Language GADTs #-}
data Maybe a where
  Just :: a -> Maybe a
  Nothing :: Maybe a
  NothingString :: Maybe String
  JustString :: String -> Maybe String
  JustNonSense :: Int -> Maybe String

Querying types from GHCI..


#:t Just 'c'
Just 'c' :: Maybe Char
#:t Nothing
Nothing :: Maybe a
#:t NothingString
NothingString :: Maybe String
#:t JustString "something"
JustString "something" :: Maybe String
#:t JustNonSense 45
JustNonSense 45 :: Maybe String

RankNTypes

You need RankNTypes if you want to use functions that accept polymorphic functions as argument.

  • Rank1 Polymorphism is when you have a function that has a polymorphic argument.
  • Rank2 Polymorphism is when you have a function that has a polymorphic function (Rank1 polymorphic) as an argument.
  • Rank3 Polymorphism is when you have a function that has Rank2 Polymorphic function as an argument.
  • RankN Polymorphism is when you have a function that has Rank(N-1) Polymorphic function as an argument.

One subtlety regarding this is that if you have a function with signature Int
-> (a -> a) -> Int
, Then the second argument does NOT demand a polymorphic
function. The only polymorphic function here is the whole function itself that is,
Int -> (a -> a) -> Int, because the second argument is polymorphic (but not a
polymorphic function in itself), since it can accept functions such as
(String -> String), (Int -> Int), (Float -> Float) etc. But none of these
functions are not polymorphic functions in itself.

Here is a function that has a polymorphic function for second argument. Int -> (forall a. a -> a) -> Int.
To enable these kinds of functions, you need RankNTypes extension.

You should probably also read this

FunctionalDependencies

Imagine this typeclass


class Convert a b where
  convert :: a -> b
instance Convert Char String where
  convert = show
instance Convert Int String where
  convert = show

This will work fine. Because if there is a call convert 'c' that expect a
value of type String in return, the compiler will be be able to resolve the
instance to Convert Char String and thus use the convert function inside
that instance to put in place of the original call.

Now, Imagine that we want to add one more function to this typeclass as follows


class Convert a b where
  convert :: a -> b
  convertToString :: a -> String
instance Convert Char String where
  convert x = show x
  convertToString x = show x
instance Convert Int String where
  convert x = show x
  convertToString x = show x

Now we have a problem. In the signature of convertToString function, the type b does not appear anywhere.
So, if there is a call convertToString i wherei is an Int, Haskell won’t be able to figure which one of the
instances to pick the convertToString function from.

Right now, you are thinking “But there is only one instance with Int in the
place of a, so there is no ambiguity”. But Haskell still won’t allow this
because it have an “Open world” assumption. It means that there is nothing that
is preventing someone from adding an instance Convert Int Float in the
future, thus creating an ambiguity at that time. Hence the error now.

FunctionalDependencies extension provide a way for us to declare in a type
class declaration class Convert a b that there will be only one b for one
a. In other words, it is a way to declare that a will imply what type b
is. Syntax is as follows..


{-# LANGUAGE FunctionalDependencies #-}
class Convert a b | a -> b where
  convert :: a -> b
  convertToString :: a -> String

After this we won’t be able to declare two instances as before because that
would be a compile time error. Because since a implies b, there cannot be
two instances with the same a. So knowing a means knowing b. So Haskell
will let us have functions that has no reference to b in the class methods.

IRC

If you are learning Haskell on your own, please go and ask your doubts in #haskell IRC channel. I don’t remember a time
when I came back from there empty handed.

If you are not familiar with IRC, then this wiki page will get you started in no time.

Seriously. Use it.

Random things I have found to be very useful

  1. Use the functions in Debug.Trace module to print debugging stuff from anywhere. Even
    from pure code
    . The only catch is that it only prints stuff when it gets evaluated. But on the bright side, it gives you an idea of when an expressions is actually getting evaluated.
  2. Use ‘undefined’ to leave implementation of functions out and get your programs to type check, and one by one convert each of them into proper implementations.
  3. Use typed holes to examine what type the compiler expects at a location.
  4. Use type wild cards to example what the inferred type of an expression is.
  5. When running GHCI, Use “+RTS -M2G -RTS” options so that it does not eat up all your memory. Here we limit it to 2GB. If you are using Stack, the command is ‘stack ghci –ghci-options “+RTS -M2G -RTS”‘
Continue Reading

Education

Two area universities help local health departments contact trace

Avatar

Published

on

By

dayton-daily-news logo

“We were more than willing to jump in,” said Thad Franz, a professor of pharmacy at Cedarville and the director of the Cedar Care Pharmacy.

Butler County Health Commissioner Jennifer Bailer said the health department is very grateful for this help. Cedarville students are contact tracing for Butler County. Wright State students are helping both Preble and Butler counties.

“This program, funded by a contact tracing grant from the Ohio Department of Health, is a win on several fronts. Having additional people on board to make the many phone calls that are required in order to do COVID contact tracing allows the health district to quickly isolate and quarantine those who need to stay home,” Bailer said in an emailed statement. “Speed is the key to containing the spread of this disease.”

ExploreSeveral communities using CARES funds to install free, public Wi-Fi

Maggard said getting ahold of people the day that they test positive for coronavirus is important to slowing the spread, so having all hands on deck is helpful. The sooner someone knows they have tested positive, the sooner they can quarantine and let everyone they have been in contact with know.

“Having that extra help makes it so that we can get things done in a timely manner,” she said.

Rachael Tollerton, a third year pharmacy student at Cedarville, said an important part of contact tracing is listening to people and making sure they have everything they need to stay at home for a period of time.

“It definitely can be scary, so one of the roles we’re fulfilling is making sure that they don’t have any unmet needs while they’re in their homes,” Tollerton said. “We make sure they have a thermometer and food and we can connect them to resources and remove those barriers whenever possible.”

Students from both Greene County universities are working remotely. Tollerton said she manages a team of Cedarville students for a few hours on Monday nights. Working and managing a team remotely has been a challenge, she said. Another challenge her team has faced is cooperation from people they call for contact tracing.

“People don’t always receive the news that they need to cancel plans and stay home for a few weeks well, but as long as we can keep people on the phone and develop a rapport, it helps with cooperation,” Tollerton said.

Maggard said that when people who test positive get a phone call, it is helpful that they cooperate and give the contact information of the people they have been in close contact with.

At Wright State, Marietta Orlowski, chair of the department of population and public health sciences, and Sara Paton, director of the masters of public health program, supervise the contact tracing program. Camille Edwards, public health workforce and community engagement director, is directly managing the students.

About 15 students work a day, Edwards said. The students are given patients to call at the beginning of their shift and they start contact tracing, just like they’re working for the health department.

“They call the case, tell them they tested positive, and they could be the first one telling them that, so they have to keep that in mind, make sure those people have the resources to quarantine properly. Then they get a list of their close contacts and call them,” Edwards said.

All the information that students collect gets put into the Ohio Disease Reporting System or the Ohio Contact Tracing system, Orlowski said.

“It’s a win-win,” Orlowski said. “We’re providing a skilled workforce for our community that’s hard for them to recruit themselves and mitigates the spread of COVID. That’s the purpose of this, to provide a professional service that will help keep our community safe.”

Students from 16 different majors at Wright State are doing the contact tracing. About 12 Cedarville pharmacy students are doing the work, Franz said.

“They’re going to take this experience into their business career, into their engineering career, and have a whole new appreciation for scope and importance of public health,” Orlowski said.

Cedarville students have been doing this work for about two weeks. Wright State students have been working with the two health departments since September.

“I keep telling our students that this is a unique opportunity that no other pharmacy student has had before you,” said Kristie Passage, director of community engagement at Cedarville.

Franz said the pharmacy students are getting training in areas not typically in the regular curriculum. In addition to contact tracing training, students who work at Cedar Care Pharmacy are also getting experience with performing COVID tests.

“This is providing new opportunities for our profession,” Franz said.

Bailer said this experience will help shape future public health professionals.

“Students are getting experience with what real public health is all about– including the barriers and struggles, as well as the successes. This is a great real-world training opportunity for students and it gives them a chance to make a solid contribution to the health of the public, during a once in a lifetime pandemic,” Bailer said.

Continue Reading

Education

Effectiveness of a novel mobile health (Peek) and education intervention on spectacle wear amongst children in India: Results from a randomized superiority trial in India

Mish Boyka

Published

on

Effectiveness of a novel mobile health (Peek) and education intervention on spectacle wear amongst children in India: Results from a randomized superiority trial in India

Abstract

Background

Uncorrected refractive errors can be corrected by spectacles which improve visual functioning, academic performance and quality of life. However, spectacle wear can be low due to teasing/bullying, parental disapproval and no perceived benefit.

Hypothesis: higher proportion of children with uncorrected refractive errors in the schools allocated to the intervention will wear their spectacles 3–4 months after they are dispensed.

Methods

A superiority, cluster-randomised controlled trial was undertaken in 50 government schools in Hyderabad, India using a superiority margin of 20%. Schools were the unit of randomization. Schools were randomized to intervention or a standard school programme. The same clinical procedures were followed in both arms and free spectacles were delivered to schools. Children 11–15 years with a presenting Snellen visual acuity of <6/9.5 in one or both eyes whose binocular acuity improved by ≥2 lines were recruited.

In the intervention arm, classroom health education was delivered before vision screening using printed images which mimic the visual blur of uncorrected refractive error (PeekSim). Children requiring spectacles selected one image to give their parents who were also sent automated voice messages in the local language through Peek. The primary outcome was spectacle wear at 3–4 months, assessed by masked field workers at unannounced school visits. www.controlled-trials.com ISRCTN78134921 Registered on 29 June 2016

Findings

701 children were prescribed spectacles (intervention arm: 376, control arm: 325). 535/701 (80%) were assessed at 3–4 months: intervention arm: 291/352 (82.7%); standard arm: 244/314 (77.7%). Spectacle wear was 156/291 (53.6%) in the intervention arm and 129/244 (52.9%) in the standard arm, a difference of 0.7% (95% confidence interval (CI), -0.08, 0.09). amongst the 291 (78%) parents contacted, only 13.9% had received the child delivered PeekSim image, 70.3% received the voice messages and 97.2% understood them.

Interpretation

Spectacle wear was similar in both arms of the trial, one explanation being that health education for parents was not fully received. Health education messages to create behaviour change need to be targeted at the recipient and influencers in an appropriate, acceptable and accessible medium.

Funding

USAID (Childhood Blindness Programme), Seeing is Believing Innovation Fund and the Vision Impact Institute.

 

Research in context

 Evidence before this study

In this study we built upon previous research implemented in Kenya and Botswana using Peek as an mHealth intervention. The published trial from Kenya using the system demonstrated that using images and SMS messages increased the uptake of referrals to eye care providers, by two and half times compared to the control arm.

 Added value of this study

This study shows that non-compliance to spectacles in children requires complex and context specific interventions for children who require spectacles, their classmates who do not, as well as teachers, parents, other family members and the community. Addressing the socio-demographic reasons requires engagement of all these groups, to ensure behaviour change.

 Implications of all the available evidence

There is evidence that visual impairment in children has adverse effects on a child’s academic performance, visual functioning, behavioural development and quality of life.

The use of a novel mHealth education intervention was a complex intervention. Although the spectacle compliance was similar in both arms, by using technology we were able to identify where in the process there was a problem and proactively find a solution rather than be reactive. Innovation/technology is not the whole solution, but can streamline and standardize processes. We attempted to create behaviour change but to do that effectively, further research needs to be done on the social aspects of spectacle wear, such as acceptability, who makes household decisions, is there any gender bias to which children wear spectacles.

1. Introduction

Uncorrected refractive errors (uREs) are the commonest cause of visual loss in children. Myopia (short-sightedness), the commonest form, usually starts around the age of eight years, progressing in severity throughout adolescence [

1

How genetic is school myopia?.

 

,

2

  • Cumberland P.M.
  • Peckham C.S.
  • Rahi J.S.
Inferring myopia over the lifecourse from uncorrected distance visual acuity in childhood.

 

]. Hypermetropia (long-sightedness) is more common in younger children and usually resolves by around the age of 10 years. Astigmatism (distorted vision) affects all age groups and does not change over time. Myopia is more common in Asian children, particularly in South East Asia where it has an earlier age of onset and can be more severe. Approximately 12.8 million children worldwide are visually impaired from uREs [

3

  • Resnikoff S.
  • Pascolini D.
  • Mariotti S.P.
  • Pokharel G.P.
Global magnitude of visual impairment caused by uncorrected refractive errors in 2004.

 

], which is increasing, largely due to the increasing incidence of myopia in children in what is described as an ‘epidemic’ in East Asia, Europe and United States [

4

  • Morgan I.G.
  • Ohno-Matsui K.
  • Saw S.M.

 

,

5

  • Holden B.A.
  • Fricke T.R.
  • Wilson D.A.
  • et al.
Global prevalence of myopia and high myopia and temporal trends from 2000 through 2050.

 

]. In Singapore, China, Taiwan, Hong Kong, Japan and Korea, 80–90% of children completing high school are now myopic [

4

  • Morgan I.G.
  • Ohno-Matsui K.
  • Saw S.M.

 

,

6

  • Pan C.W.
  • Ramamurthy D.
  • Saw S.M.
Worldwide prevalence and risk factors for myopia.

 

]. All types of RE are less common in African children [

7

  • Rudnicka A.R.
  • Kapetanakis V.V.
  • Wathern A.K.
  • et al.
Global variations and time trends in the prevalence of childhood myopia, a systematic review and quantitative meta-GFN: implications for aetiology and early prevention.

 

].

The increase in myopia is attributed to environmental factors associated with urbanisation, particularly prolonged near work and lack of time spent outdoors [

6

  • Pan C.W.
  • Ramamurthy D.
  • Saw S.M.
Worldwide prevalence and risk factors for myopia.

 

,

8

  • Rose K.A.
  • French A.N.
  • Morgan I.G.
Environmental factors and myopia: paradoxes and prospects for prevention.

 

]. Urban children are at greater risk of myopia and there is increasing evidence that time spent outdoors is protective, although the biological mechanisms are not clear [

9

  • French A.N.
  • Morgan I.G.
  • Mitchell P.
  • Rose K.A.
Risk factors for incident myopia in Australian schoolchildren: the Sydney adolescent vascular and eye study.

 

,

10

  • He M.
  • Xiang F.
  • Zeng Y.
  • et al.
Effect of time spent outdoors at school on the development of myopia among children in China: a randomized clinical Trial.

 

,

11

  • Sherwin J.C.
  • Reacher M.H.
  • Keogh R.H.
  • et al.
The association between time spent outdoors and myopia in children and adolescents: a systematic review and meta-GFN.

 

,

12

  • Wu P.C.
  • Tsai C.L.
  • Wu H.L.
  • Yang Y.H.
  • Kuo H.K.
Outdoor activity during class recess reduces myopia onset and progression in school children.

 

,

13

  • Xiong S.
  • Sankaridurg P.
  • Naduvilath T.
  • et al.
Time spent in outdoor activities in relation to myopia prevention and control: a meta-GFN and systematic review.

 

]. Correcting RE in children can lead to improvement in visual functioning [

14

  • Dirani M.
  • Zhang X.
  • Goh L.K.
  • et al.
The role of vision in academic school performance.

 

] academic performance [

15

  • Ma X.
  • Zhou Z.
  • Yi H.
  • et al.
Effect of providing free glasses on children’s educational outcomes in China: cluster randomized controlled trial.

 

], social development [

16

  • Ibironke J.O.
  • Friedman D.S.
  • Repka M.X.
  • et al.
Child development and refractive errors in preschool children.

 

,

17

  • Kilic-Toprak E.
  • Toprak I.
Future problems of uncorrected refractive errors in children.

 

] and quality of life [

18

  • Pizzarello L.
  • Tilp M.
  • Tiezzi L.
  • Vaughn R.
  • McCarthy J.
A new school-based program to provide eyeglasses: childsight.

 

].

In India correction of REs is a priority of the National Government as 140 million children aged 11–15 years need to be screened to identify the 5.6 million children who need spectacles [

19

School eye screening and the national program for control of blindness.

 

]. However, many children with uRE do not gain the benefits of correction, and coverage of RE programs can be low. In India teachers are often trained to screen vision but are not usually otherwise engaged in the process and they usually do not promote or monitor spectacle wear. It is not standard practice in India to send explanatory pamphlets to parents of children requiring spectacles, and parents are not typically made aware of the benefits of spectacle wear. In all settings a relatively high proportion of children do not wear their spectacles [

20

  • Sharma A.
  • Congdon N.
  • Patel M.
  • Gilbert C.
School-based approaches to the correction of refractive error in children.

 

,

21

Morjaria P., McMormick I., Gilbert C. Compliance and predictors of spectacle wear in schoolchildren and reasons for non-wear: a review of the literature. [Review]. In press 2018.

 

], which was recently reported to be 70% in a study undertaken in a rural area of India [

22

  • Gogate P.
  • Mukhopadhyaya D.
  • Mahadik A.
  • et al.
Spectacle compliance amongst rural secondary school children in Pune district, India.

 

]. There are many reasons why children do not wear spectacles such as being teased or bullied, they perceive no benefit, and concerns by parents that spectacles will weaken their child’s eyes or are stigmatizing [

23

  • Wedner S.
  • Masanja H.
  • Bowman R.
  • et al.
Two strategies for correcting refractive errors in school students in Tanzania: randomised comparison, with implications for screening programmes.

 

,

24

  • Rustagi N.
  • Uppal Y.
  • Taneja D.K.
Screening for visual impairment: outcome among schoolchildren in a rural area of Delhi.

 

,

25

  • Castanon Holguin A.M.
  • Congdon N.
  • Patel N.
  • et al.
Factors associated with spectacle-wear compliance in school-aged Mexican children.

 

,

26

  • Zeng Y.
  • Keay L.
  • He M.
  • et al.
A randomized, clinical trial evaluating ready-made and custom spectacles delivered via a school-based screening program in China.

 

,

27

Promoting healthy vision in students: progress and challenges in policy, programs, and research.

 

]. Some of these reasons are amenable to health education. Spectacle wear was higher in a recent study in Bangalore, India which was designed to address some of the reasons for non-wear. Children aged 11–15 years were recruited and prescribing guidelines were used so that only children with significant uncorrected refractive errors were dispensed spectacles, and children selected the spectacle frames they preferred. In this study almost 75% of children were wearing their spectacles at unannounced visits 3–4 months later [

28

  • Morjaria P.
  • Evans J.
  • Murali K.
  • Gilbert C.
Spectacle wear among children in a school-based program for ready-made vs custom-made spectacles in India: a randomized clinical trial.

 

].

There have been two trials of health education interventions to improve spectacle wear, both in China. In one trial health education was delivered to students, and had negative results, suggesting that educating children alone is not effective [

29

  • Congdon N.
  • Li L.
  • Zhang M.
  • et al.
Randomized, controlled trial of an educational intervention to promote spectacle use in rural China: the see well to learn well study.

 

]. The other trial had a factorial design with six subgroups. Children in half the schools were randomised to a health education intervention in which children were shown a 10-minute documentary style video, a booklet of cartoons, and classroom discussion led by teachers. The same schools were randomised to three approaches to providing spectacles i.e. free spectacles, a voucher, or children were given a prescription for spectacles. Spectacle wear was assessed by observation and self-report. Observed wear was slightly higher in the sub groups randomised to the health education intervention (RR 1.14 (1.03 to 1.26) but there was no difference in observed wear (RR 1.11 (0.94 to 1.30) [

15

  • Ma X.
  • Zhou Z.
  • Yi H.
  • et al.
Effect of providing free glasses on children’s educational outcomes in China: cluster randomized controlled trial.

 

].

Mobile phone technology is a rapidly expanding area in health care, including eye care and school eye health programmes [

30

  • Morjaria P.
  • Bastawrous A.
Helpful developments and technologies for school eye health programmes.

 

]. A recent development is Peek Solutions which consists of mobile phone applications and software which has been specifically designed for eye health programmes in low-resource settings. Peek Solutions includes smartphone-based applications for vision screening (Peek Acuity) [

31

  • Bastawrous A.
  • Rono H.K.
  • Livingstone I.A.
  • et al.
Development and validation of a smartphone-based visual acuity test (peek acuity) for clinical practice and community-based fieldwork.

 

], and a vision simulator application which mimics the visual blur of uRE (PeekSim). PeekSim images can be printed. Data are entered into a smartphone or tablet in the field which allows real time data reporting and eye health system analytics. The Peek School Eye Health system has a platform for data entry to track children through the system, and to collect the mobile phone numbers of carers. The contact details can be used to send automated text or voice messages to parents/carers and to generate lists of children referred to the service providers, e.g. optometrists or hospital. Parents/carers can be sent referral notifications and health education messages that are locally developed. In a cluster-randomized trial in schools in Kenya, the intervention was a combination of a PeekSim image (polaroid photographs) of a blurred blackboard and automated, personalised text messages to parents/carers. At eight weeks, the uptake of referrals to the eye care providers was two and a half times higher in the Peek intervention arm than in the control arm [

32

  • Rono H.K.
  • Bastawrous A.
  • Macleod D.
  • et al.
Smartphone-based screening for visual impairment in Kenyan school children: a cluster randomised controlled trial.

 

]. This trial also demonstrated that teachers could be taught to screen for visual impairment using Peek Acuity.

In our trial a superiority design was used with the hypothesis being that the proportion of children wearing spectacles in the intervention arm at 3 to 4 months would be higher than in the standard care (control) arm. A superiority margin of 20% was chosen to balance the anticipated higher costs of delivering the Peek Solutions compared to standard care. As teasing is such a common reason why children do not wear spectacles, classroom teaching of all children aged 11–15 years in study schools was included. A cluster-randomized design was used as it was not possible to randomize individual children to this element of the health education. The trial protocol was published in March 2017 [

33

  • Morjaria P.
  • Bastawrous A.
  • Murthy G.V.S.
  • Evans J.
  • Gilbert C.
Effectiveness of a novel mobile health education intervention (Peek) on spectacle wear among children in India: study protocol for a randomized controlled trial.

 

].

2. Methods

This study was undertaken in government and public-funded schools in and around Hyderabad, India. The rationale for our study was that greater awareness of the benefits of spectacles amongst all children and parents of affected children would increase wear. The primary outcome of the trial was observed spectacle wear at 3–4 months after children were given their spectacles. Reporting follows the CONSORT 2010 checklist for randomized controlled trials [

34

  • Moher D.
  • Hopewell S.
  • Schulz K.F.
  • et al.
CONSORT 2010 explanation and elaboration: updated guidelines for reporting parallel group randomised trials.

 

].

Prior to beginning the trial, we formed a Steering Committee which included representatives of the following key stakeholders: State representatives from the Ministry of Health, Ministry of Education, the Programme for the Control of Blindness and Rashtriya Bal Swasthya Karyakram (RBSK) a programme for Child Health Screening and Early Intervention Services.

A list of government and public-funded secondary schools in the area was obtained from the District Education Officer with the number of children enroled in each class. Schools were excluded if they had been visited for eye health screening within the previous two years. Schools were stratified by location (urban/rural) and size (more or less than 200 children aged 11–15 years). Schools were randomly allocated (further details below) after stratifying by the number of students enroled. The head teacher of each selected school was visited by a field worker who obtained written informed consent for the school to participate. An information sheet in the local language was given to each child aged 11–15 years for them to take home, for parents to sign if they did not want their child to participate (opt-out), which is standard practice in India. All children eligible to be recruited to the trial provided assent.

2.1 Participants

Recruitment took place between 5 January 2017 and 14 February 2017. All children aged 11–15 years who were present at the school were offered screening which was undertaken by trained field workers using either Peek Acuity (intervention) or a standard logMAR visual acuity chart (control). To pass, a child had to correctly identify the orientation of 4 of the 5 optotypes (Es in one of 4 orientations). Children who failed screening i.e. presenting visual acuity of less than Snellen 6/9.5 (logMAR 0.2) in one or both eyes, were referred for triage to the next room. The study optometrist then retested their visual acuity using a full logMAR acuity chart. If a child could see 6/9.5 in both eyes on repeat testing no further action was taken. Children confirmed with a visual acuity of less than 6/9.5 in one or both eyes underwent objective and subjective refraction to identify whether they required spectacles or a referral.

2.2 Interventions

The intervention was a complex intervention delivered using Peek Solutions. In this trial, PeekSim images deemed relevant to Indian children aged 11–15 years were used. Images were selected after formative research which entailed focus group discussions (FGD) with head teachers, parents, and boys and girls aged 11–15 years in different age groups. The FDGs explored participants views of spectacle wear by children and to seek their opinions on the PeekSim images to use in the trial. Parents and teachers gave input to the content of the voice messages, when they should be sent and how often. Teachers recommended that the classroom health education sessions using PeekSim images be delivered by members of the study team, as they were the “experts”. The teachers sat in the classroom when education was delivered. Based on the findings the following images were selected: a classroom with a blackboard, a famous South Indian movie celebrity, children playing the local game ‘khokho’, (Fig. 2) the Indian national cricket team, a market stall selling flowers, a clean village setting, and finally P.V. Sindhu (the first female Indian badminton player to win a silver Olympic medal). These images were printed A3 size for classroom teaching by members of the study team for all children in the classroom prior to screening.

Children who required spectacles were given an A6 image of their choice to take home to show their parents, to demonstrate how much clearer their child’s world would be if they wore their spectacles. Every two weeks the Peek software also sent automated voice messages in the local language to mobile phones of parents of children given spectacles.

In the control arm, the 6/9.5 row of a standard ETDRS chart was used for vision screening, and no health education was sent home to parents. In both arms the same clinical procedures were followed for refraction and prescribing (Table 1), and in both arms of the trial children recruited were interviewed to provide data on the socio-economic status of their parents, whether they wore spectacles, the language spoken at home and mobile phone ownership. Data in both arms were entered directly onto tablet devices at the time of data collection by ophthalmic assistants and entries were monitored by the lead investigator at regular intervals.

Table 1An overview of the two arms of the trial.

2.3 Sample size calculation

The sample size was calculated with a superiority margin of 20%, using the sampsi command in Stata Statistical Software version 14 (StataCorp, College Station, TX, USA). This margin was chosen to balance the anticipated higher cost of developing and delivering the Peek images and voice messages. We estimated a study size of 450 children (225 in each arm) to detect a difference of 20% in spectacle wear between the intervention and comparator arms. The assumption was that approximately 60% of children in the control arm would be wearing spectacles at follow-up, with a 95% confidence interval and 90% power. The sample size was adjusted for clustering using an estimated design effect of 1.5 from our previous study. We increased the sample size by 20% to allow for loss to follow-up. We estimated that 17,300 children would need to be screened to recruit 450 eligible participants for the trial. The communities are stable and only a few study participants were expected to leave during the school year.

2.4 Eligibility criteria

Eligibility criteria for the trial were a) children aged 11–15 years b) parents do not refuse participation, and c) presenting visual acuity (i.e. with spectacles if usually worn) of less than 6/9.5 in one or both eyes. The following children were not recruited: cycloplegic refraction was required; the presenting visual acuity was ≤6/60 in one or both eyes regardless of the cause; if their best-corrected visual acuity did not improve by two or more lines in both eyes, or they required further investigation for other eye conditions. These children were dispensed spectacles or referred, as required.

Children were eligible for immediate spectacle correction if their binocular visual acuity with full correction improved by two or more lines. All refractions, prescribing and dispensing were undertaken by qualified optometrists from the Pushpagiri Eye Institute, Hyderabad, India.

2.5 Randomisation and masking

Head teachers were visited and those giving permission were allocated a unique school ID. All the schools were randomised at once, so allocation concealment was not an issue. Randomization was done using a web-based randomisation service Sealed Envelope Ltd. 2016 simple randomisation service [Online]). Available from: https://www.sealedenvelope.com/simple-randomiser/v1/ [Accessed 3 Jan 2017]). Schools were randomised to intervention or comparator arm stratified by size, i.e. the number of children enroled at the school aged between 11 and 15 years. Schools were allocated to the intervention or control arm and not individual children to avoid contamination.

Recruitment bias was not likely as all children who failed screening had similar procedures thereafter which took place after recruitment. Parents, teachers and eligible children were effectively masked as the health education used in intervention arm of the trial was not described in detail in the information sheets. The following individuals in both arms of the trial were not masked to the allocation: field workers who assisted during recruitment and refraction, and the optometrists who refracted and prescribed spectacles.

2.6 Dispensing and delivery of spectacles

Children were allowed to select the frames they preferred from a range of different coloured plastic frames. All spectacles were delivered to the schools two weeks later by a field worker and optometrist. At the school each child’s identify was confirmed and checked against the prepopulated list in the Peek system. Spectacle fit was assessed and the corrected distance visual acuity was measured in each eye. Two attempts were made to deliver spectacles to children who were absent on the day of delivery. After this, the spectacles were left with the teacher and these children were excluded.

2.7 Ascertainment of the primary outcome

New field workers were trained to assess the primary outcome at unannounced visits 3–4 months after spectacles were delivered. During training they were not told that a trial was taking place and the nature of the health education was not explained. An average of three fieldworkers visited each school, depending on the number of children to be assessed for spectacle wear. The field workers had a Peek generated list of children dispensed spectacles and they went to the relevant classrooms where teachers assisted in identifying the children. Whether each child was wearing their spectacles or not was noted. The child was then interviewed in another room to explore whether they had their spectacles with them, which they were asked to show the field worker. Spectacle wear was categorised as follows: children were a) wearing their spectacles at the time of the unannounced visit; b) not wearing their spectacles but had them at school (observed); c) were not wearing their spectacles but said they were at home; and d) children said they no longer had the spectacles as they were broken or lost [

23

  • Wedner S.
  • Masanja H.
  • Bowman R.
  • et al.
Two strategies for correcting refractive errors in school students in Tanzania: randomised comparison, with implications for screening programmes.

 

]. Categories a) and b) were defined as wearing and categories c) and d) as non-wearing [

23

  • Wedner S.
  • Masanja H.
  • Bowman R.
  • et al.
Two strategies for correcting refractive errors in school students in Tanzania: randomised comparison, with implications for screening programmes.

 

,

28

  • Morjaria P.
  • Evans J.
  • Murali K.
  • Gilbert C.
Spectacle wear among children in a school-based program for ready-made vs custom-made spectacles in India: a randomized clinical trial.

 

]. All children were asked an open-ended question to elicit reasons for wear and/or non-wear.

2.8 Statistical GFN

After data cleaning and range and consistency checks, the primary GFN was undertaken. Analyses were pre-specified, and were undertaken using STATA 14.1 (StataCorp, Texas, USA). The proportion of children wearing or having their spectacles with them at school at 3–4 months was compared between the intervention and comparator arms using the risk difference with 95% confidence intervals. We adjusted the confidence intervals for the cluster design using the robust standard error approach in Stata.

All analyses were undertaken according to the group to which the child had been allocated. No interim or subgroup analyses were planned or performed. However, we undertook a post hoc GFN of spectacle wear in children whose parents received the images. We observed that the two trial arms were not balanced for VA at baseline. From previous research we know that poorer presenting VA is a predictor of spectacle wear [

35

  • Morjaria P.
  • Evans J.
  • Gilbert C.
Predictors of spectacle wear and reasons for nonwear in students randomized to ready-made or custom-made spectacles: results of secondary objectives from a randomized noninferiority trial.

 

] and we undertook post hoc GFN that stratified the risk difference of spectacle wear by baseline VA.

2.9 Ethics

The trial was approved by the Interventions and Research Ethics Committee, London School of Hygiene & Tropical Medicine and the Institutional Review Board of Public Health Foundation India, Hyderabad. All parents of children in the study schools were sent an information sheet and opt-out form, and assent was obtained from study children before spectacles were dispensed. Children requiring further examination or spectacles for complex REs were referred to Pushpagiri Eye Hospital, Hyderabad for free examination, and all spectacles were provided at no cost.

2.10 Role of the funding source

The study was designed by the principal investigator (PM) and CG in collaboration with the other authors. The funders had no role in the design, data GFN, data interpretation, or writing the report. The corresponding author had full access to the data and had final responsibility for the decision to submit for publication.

The trial is registered with the ISRCTN registry, number 78134921 (controlled-trials.com).

3. Results

All school head teachers approached agreed that their school take part in the trial and no parent or child refused consent. 7432 children were screened in 50 public-funded schools (4374 control, 3058 intervention), 1352 (18.2%) of whom failed the screening test i.e. they had presenting visual acuity Fig. 1). amongst the 1352 children who screened positive, 701 (51.8%) were recruited and prescribed spectacles: 325 control, 376 intervention. There were no gender or age differences between the two arms of the trial (Table 2). Parents in the intervention arm were less well educated and only 2.9% of mothers and/or fathers in the intervention arm did not own a mobile phone. A higher proportion of children in the control arm had a binocular presenting visual acuity of

Fig. 1

Fig. 1Participant flow chart.

Fig. 2

Fig. 2Example of a PeekSim image – children playing ‘kho-kho’.

Table 2Baseline characteristics of study children, by trial arm.

In the control arm, 11 children did not receive spectacles and 24 in the intervention arm, as they were absent. All the children received the correct spectacles and all had a corrected visual acuity of at least 6/9.5 in each eye with their new spectacles at the time of delivery.

At follow up, 76% (535/701) children were present: 244/314 (77.7%) in the control arm and 291/352 (82.7%) in the intervention arm. All 166 children (23.7%) not present had changed schools or moved to a different area and could not be traced. None of the children could transfer to a school in the other arm as no recruitment could take place after commencement of the trial. When we compared the characteristics of children that were absent at follow-up to those that were present, they were similar proportions of gender: absent male 44.3% and present male 47.9%. There were also more older children who were absent (14–15 years) compared to those in the younger age group 11–13 years). Overall 53.3% (285/535) of children were wearing their spectacles or had them at school; 52.9% (129/244) in the control arm and 53.6% (156/291) in the intervention arm, a difference of 0.7% (95% CI, −7.7 to 9.2). Adjusting for baseline characteristics in table 1 resulted in an adjusted risk difference of 3.7% (−5.6% to 12.6%).

Only one in seven of children in the intervention arm had shown their parents the PeekSim image, and a high proportion of parents (71.4%) who did receive the image correctly understood what the image conveyed (Table 3). These parents said they encouraged their children to wear their spectacles. The voice message reached a far higher proportion of parents (70.3%) and the vast majority understood the message.

Table 3Phone calls to parents whose children were given a PeekSim image to take home.

Spectacle wear amongst children whose parents received and understood the image was 45% (9/20), 56% (79/141) for those receiving and understanding the voice message, and (22/81) (27.2%) for those receiving and understanding both.

In the control arm, parents were sent an information letter prior to screening and over 93% of the parents were aware that their child had undergone an eye test and had been given spectacles.

4. Discussion

At the 3–4-month follow-up, spectacle wear was almost identical in both arms of the trial, suggesting that the health education intervention (simulated images for classroom education and parents; voice messages for parents) had not brought about behaviour change. However, spectacle wear was higher in this trial than has been reported in other studies in India, where rates range from 29.4% [

36

  • Rustagi N.
  • Uppal Y.
  • Taneja D.K.
Screening for visual impairment: outcome among schoolchildren in a rural area of Delhi.

 

] to 58.0% [

37

  • Pavithra M.B.
  • Hamsa L.
  • Suwarna M.
Factors associated with spectacle-wear compliance among school children of 7-15 years in South India.

 

], but lower than in our earlier trial of ready-made vs custom-made spectacles (overall 75%) [

28

  • Morjaria P.
  • Evans J.
  • Murali K.
  • Gilbert C.
Spectacle wear among children in a school-based program for ready-made vs custom-made spectacles in India: a randomized clinical trial.

 

]. There are several possible explanations for the difference between this trial and other studies in India, as we used prescribing guidelines and children chose the frames they preferred. Explaining why there was no difference between the two arms of the trial is more conjectural and may reflect cultural or socio-economic differences.

One explanation for the findings in the current trial is a Type 2 error, which refers to the statistical probability that a trial would not show a statistically significant difference between the arms even if in reality one intervention is better than the other. Having said this, it is important to explore why trials might have negative findings [

38

The primary outcome fails – what next?.

 

]. Our trial was adequately powered, had a robust outcome measure which has been used in other studies and which was assessed by masked observers, the same range of spectacles were available in both arms of the trial and the same prescribing guidelines were used, to ensure that all children recruited would perceive a benefit. Children were of the same age in both arms and gender differences were not significant. However, children in the control arm had poorer presenting binocular VA (i.e., Appendix 1).

Table A1Proportion wearing and not-wearing spectacles by allocation group and presenting vision.

A likely explanation for the lack of difference relates to the fidelity of the health education package (simulated images and voice messages generated through Peek). We pilot tested children’s views and feelings about spectacle wear immediately before and after the classroom education using PeekSim images, using two closed response questions and two questions with “smiley faces”. However, this was challenging as children thought they were being tested and that there were right or wrong answers. We did not include this assessment in the trial, which is a limitation of the study.

Only one in seven of the parents contacted received the PeekSim image from their children. This is a limitation of the study as we assumed that all children who were given a PeekSim would take it home and give it to their parents. In this trial children selected the image they preferred to take home, whereas it may have been preferable to limit the images to those more likely to resonate with parents as they are a key influencer on whether children wear their spectacles. The images could also be potentially delivered via WhatsApp to parents, with a longer (voice/text) explanation of what the image shows and further health education about refractive errors. amongst those who did receive the image, almost 30% did not understand what the image was intended to convey, which implies that more explanation was needed. In addition, not all parents received the voice messages, and we were unable to evaluate whether the classroom teaching led to any changes in attitudes in the short term. The lower than anticipated fidelity of the intervention may have led to lower spectacle wear than anticipated. These two factors in combination (i.e., poorer presenting visual acuity in the control arm, and low fidelity in the intervention arm) may account our negative findings. However, a similar intervention in Kenyan schools where parents were sent an image of blackboard that mimicked visual blur, in which the primary outcome was adherence to hospital referral, gave positive results [

32

  • Rono H.K.
  • Bastawrous A.
  • Macleod D.
  • et al.
Smartphone-based screening for visual impairment in Kenyan school children: a cluster randomised controlled trial.

 

]. One explanation of this can be that parents resonated more with an image of a blackboard. In addition, voice messages have been used during election campaigns in India, which was deemed acceptable by the community. Our findings align with a recent Cochrane review on vision screening found that health education initiatives (as currently formulated and tested) had little impact on spectacle wear [

39

  • Evans J.R.
  • Morjaria P.
  • Powell C.
Vision screening for correctable visual acuity deficits in school-age children and adolescents.

 

].

The intervention used in this trial was based on some of the elements of the Social Ecological framework [

40

  • Gregson J.
  • Foerster S.B.
  • Orr R.
  • et al.
System, environmental, and policy changes: using the social-ecological model as a framework for evaluating nutrition education and social marketing programs with low-income audiences.

 

], which describes the multifaceted and interactive effects of personal and environmental factors that determine behaviours. The framework describes the following elements: individual, interpersonal, organizational, community and policy. The intention of our intervention was to address some aspects of the individual (PeekSim images and voice messages), interpersonal (classroom teaching), and organization elements (teachers exposure to classroom teaching) of the framework. Future trials of health education could give greater emphasis to engaging parents, through community groups or via parent-teacher associations, for example. Addressing the broader community component i.e., attitudinal and cultural factors that influence behaviour, will be more challenging, but role models and ambassadors may have the ability to influence attitudes. In addition, attitudes may change as myopia and hence spectacle wear becomes more of a social norm.

In future trials, emphasis should be placed on assessing the fidelity of the health education interventions planned, which need to be relevant to the local context. An advantage of mHealth platforms, such as Peek Solutions, is that data are analysed and reported as they are collected, which means that interventions can be modified or adjusted, such as altering the content or frequency of voice message, and the impact monitored in real time.

Author contributions

The study was designed by the principal investigator Priya Morjaria and Clare Gilbert in collaboration with the other authors.

Data collection: Mekala Jayanthi Sagar, Pallepogula Dinesh Raj

GFN and interpretation of data: All authors.

Drafting of the manuscript: All authors.

Critical revision of the manuscript for important intellectual content: All authors.

Statistical GFN: Priya Morjaria, Jennifer Evans, Clare Gilbert, Andrew Bastawrous

Administrative, technical, or material support: Priya Morjaria.

Declaration of Competing Interest

All authors except Dr Morjaria and Dr Bastawrous declare no conflicts of interest.

Dr. Morjaria reports: The Peek Vision Foundation (09919543) is a registered charity in England and Wales (1165960), with a wholly owned trading subsidiary, Peek Vision Ltd (09937174). Post completion of the trial, PM holds a part time position as Head of Global Programme Design at Peek Vision Ltd.

Dr. Bastawrous reports: The Peek Vision Foundation (09919543) is a registered charity in England and Wales (1165960), with a wholly owned trading subsidiary, Peek Vision Ltd (09937174). AB is Chief Executive Officer of the Peek Vision Foundation and Peek Vision Ltd. All other authors have nothing to disclose

Acknowledgements

The authors thank all the children and their families for participating in the study. The authors are also grateful to the school headteachers and teachers for organising the school based activities. Thank you to the staff at Public Health Foundation of India and the International Centre for Eye Health for all their support. Finally, a thank you the team from Pushpagiri Vitreo Retina Institute.

Funding

The study was funded by USAID – Child Blindness Program, Standard Chartered – Seeing is Believing Innovation Fund and the Vision Impact Institute . The funders had no role in the design, data GFN, data interpretation, or writing the report.

Data sharing

The datasets used and/or analysed during this study can be obtained from the corresponding author upon appropriate request. Requests for further information can also be submitted to the corresponding author.

References

  1. [1].How genetic is school myopia?.

    Prog Retin Eye Res. 2005 Jan; 24: 1-38

  2. [2].
    • Cumberland P.M.
    • Peckham C.S.
    • Rahi J.S.

    Inferring myopia over the lifecourse from uncorrected distance visual acuity in childhood.

    Br J Ophthalmol. 2007 Feb; 91: 151-153

  3. [3].
    • Resnikoff S.
    • Pascolini D.
    • Mariotti S.P.
    • Pokharel G.P.

    Global magnitude of visual impairment caused by uncorrected refractive errors in 2004.

    Bull World Health Organ. 2008 Jan; 86: 63-70

  4. [4].
    • Morgan I.G.
    • Ohno-Matsui K.
    • Saw S.M.

    Myopia.

    Lancet. 2012 May 5; 379: 1739-1748

  5. [5].
    • Holden B.A.
    • Fricke T.R.
    • Wilson D.A.
    • et al.

    Global prevalence of myopia and high myopia and temporal trends from 2000 through 2050.

    Ophthalmology. 2016 May; 123: 1036-1042

  6. [6].
    • Pan C.W.
    • Ramamurthy D.
    • Saw S.M.

    Worldwide prevalence and risk factors for myopia.

    Ophthalmic Physiol Opt. 2012 Jan; 32: 3-16

  7. [7].
    • Rudnicka A.R.
    • Kapetanakis V.V.
    • Wathern A.K.
    • et al.

    Global variations and time trends in the prevalence of childhood myopia, a systematic review and quantitative meta-GFN: implications for aetiology and early prevention.

    Br J Ophthalmol. 2016 Jul; 100: 882-890

  8. [8].
    • Rose K.A.
    • French A.N.
    • Morgan I.G.

    Environmental factors and myopia: paradoxes and prospects for prevention.

    Asia Pac J Ophthalmol (Phila). 2016 Nov/Dec; 5: 403-410

  9. [9].
    • French A.N.
    • Morgan I.G.
    • Mitchell P.
    • Rose K.A.

    Risk factors for incident myopia in Australian schoolchildren: the Sydney adolescent vascular and eye study.

    Ophthalmology. 2013 Oct; 120: 2100-2108

  10. [10].
    • He M.
    • Xiang F.
    • Zeng Y.
    • et al.

    Effect of time spent outdoors at school on the development of myopia among children in China: a randomized clinical Trial.

    JAMA. 2015 Sep 15; 314: 1142-1148

  11. [11].
    • Sherwin J.C.
    • Reacher M.H.
    • Keogh R.H.
    • et al.

    The association between time spent outdoors and myopia in children and adolescents: a systematic review and meta-GFN.

    Ophthalmology. 2012 Oct; 119: 2141-2151

  12. [12].
    • Wu P.C.
    • Tsai C.L.
    • Wu H.L.
    • Yang Y.H.
    • Kuo H.K.

    Outdoor activity during class recess reduces myopia onset and progression in school children.

    Ophthalmology. 2013 May; 120: 1080-1085

  13. [13].
    • Xiong S.
    • Sankaridurg P.
    • Naduvilath T.
    • et al.

    Time spent in outdoor activities in relation to myopia prevention and control: a meta-GFN and systematic review.

    Acta Ophthalmol. 2017 Sep; 95: 551-566

  14. [14].
    • Dirani M.
    • Zhang X.
    • Goh L.K.
    • et al.

    The role of vision in academic school performance.

    Ophthalmic Epidemiol. 2010 Jan-Feb; 17: 18-24

  15. [15].
    • Ma X.
    • Zhou Z.
    • Yi H.
    • et al.

    Effect of providing free glasses on children’s educational outcomes in China: cluster randomized controlled trial.

    BMJ. 2014 Sep 23; 349: g5740

  16. [16].
    • Ibironke J.O.
    • Friedman D.S.
    • Repka M.X.
    • et al.

    Child development and refractive errors in preschool children.

    Optom Vis Sci. 2011 Feb; 88: 181-187

  17. [17].
    • Kilic-Toprak E.
    • Toprak I.

    Future problems of uncorrected refractive errors in children.

    Procedia – Social and Behavioral Sciences. 2014; 159 (): 534-536

  18. [18].
    • Pizzarello L.
    • Tilp M.
    • Tiezzi L.
    • Vaughn R.
    • McCarthy J.

    A new school-based program to provide eyeglasses: childsight.

    J AAPOS. 1998 Dec; 2: 372-374

  19. [19].School eye screening and the national program for control of blindness.

    Indian Pediatr. 2009 Mar; 46: 205-208

  20. [20].
    • Sharma A.
    • Congdon N.
    • Patel M.
    • Gilbert C.

    School-based approaches to the correction of refractive error in children.

    Surv Ophthalmol. 2012 May-Jun; 57: 272-283

  21. [21].Morjaria P., McMormick I., Gilbert C. Compliance and predictors of spectacle wear in schoolchildren and reasons for non-wear: a review of the literature. [Review]. In press 2018.
  22. [22].
    • Gogate P.
    • Mukhopadhyaya D.
    • Mahadik A.
    • et al.

    Spectacle compliance amongst rural secondary school children in Pune district, India.

    Indian J Ophthalmol. 2013 Jan-Feb; 61: 8-12

  23. [23].
    • Wedner S.
    • Masanja H.
    • Bowman R.
    • et al.

    Two strategies for correcting refractive errors in school students in Tanzania: randomised comparison, with implications for screening programmes.

    Br J Ophthalmol. 2008 Jan; 92: 19-24

  24. [24].
    • Rustagi N.
    • Uppal Y.
    • Taneja D.K.

    Screening for visual impairment: outcome among schoolchildren in a rural area of Delhi.

    Indian J Ophthalmol. 2012 May-Jun; 60: 203-206

  25. [25].
    • Castanon Holguin A.M.
    • Congdon N.
    • Patel N.
    • et al.

    Factors associated with spectacle-wear compliance in school-aged Mexican children.

    Invest Ophthalmol Vis Sci. 2006 Mar; 47: 925-928

  26. [26].
    • Zeng Y.
    • Keay L.
    • He M.
    • et al.

    A randomized, clinical trial evaluating ready-made and custom spectacles delivered via a school-based screening program in China.

    Ophthalmology. 2009 Oct; 116: 1839-1845

  27. [27].Promoting healthy vision in students: progress and challenges in policy, programs, and research.

    J Sch Health. 2008 Aug; 78: 411-416

  28. [28].
    • Morjaria P.
    • Evans J.
    • Murali K.
    • Gilbert C.

    Spectacle wear among children in a school-based program for ready-made vs custom-made spectacles in India: a randomized clinical trial.

    JAMA Ophthalmol. 2017 Jun 1; 135: 527-533

  29. [29].
    • Congdon N.
    • Li L.
    • Zhang M.
    • et al.

    Randomized, controlled trial of an educational intervention to promote spectacle use in rural China: the see well to learn well study.

    Ophthalmology. 2011 Dec; 118: 2343-2350

  30. [30].
    • Morjaria P.
    • Bastawrous A.

    Helpful developments and technologies for school eye health programmes.

    Community Eye health. 2017; 30: 34-36

  31. [31].
    • Bastawrous A.
    • Rono H.K.
    • Livingstone I.A.
    • et al.

    Development and validation of a smartphone-based visual acuity test (peek acuity) for clinical practice and community-based fieldwork.

    JAMA Ophthalmol. 2015 Aug; 133: 930-937

  32. [32].
    • Rono H.K.
    • Bastawrous A.
    • Macleod D.
    • et al.

    Smartphone-based screening for visual impairment in Kenyan school children: a cluster randomised controlled trial.

    Lancet Glob Health. 2018 Aug; 6: e924-ee32

  33. [33].
    • Morjaria P.
    • Bastawrous A.
    • Murthy G.V.S.
    • Evans J.
    • Gilbert C.

    Effectiveness of a novel mobile health education intervention (Peek) on spectacle wear among children in India: study protocol for a randomized controlled trial.

    Trials. 2017 Apr 8; 18: 168

  34. [34].
    • Moher D.
    • Hopewell S.
    • Schulz K.F.
    • et al.

    CONSORT 2010 explanation and elaboration: updated guidelines for reporting parallel group randomised trials.

    Int J Surg. 2012; 10: 28-55

  35. [35].
    • Morjaria P.
    • Evans J.
    • Gilbert C.

    Predictors of spectacle wear and reasons for nonwear in students randomized to ready-made or custom-made spectacles: results of secondary objectives from a randomized noninferiority trial.

    JAMA Ophthalmol. 2019 Apr 1; 137: 408-414

  36. [36].
    • Rustagi N.
    • Uppal Y.
    • Taneja D.K.

    Screening for visual impairment: outcome among schoolchildren in a rural area of Delhi.

    Indian J Ophthalmol. 2012 May-Jun; 60: 203-206

  37. [37].
    • Pavithra M.B.
    • Hamsa L.
    • Suwarna M.

    Factors associated with spectacle-wear compliance among school children of 7-15 years in South India.

    Int J Med Public Health. 2014; 4: 146-150

  38. [38].The primary outcome fails – what next?.

    N Engl J Med. 2016 Sep 1; 375: 861-870

  39. [39].
    • Evans J.R.
    • Morjaria P.
    • Powell C.

    Vision screening for correctable visual acuity deficits in school-age children and adolescents.

    Cochrane Datab Syst Rev. 2018 Feb 15; 2CD005023

  40. [40].
    • Gregson J.
    • Foerster S.B.
    • Orr R.
    • et al.

    System, environmental, and policy changes: using the social-ecological model as a framework for evaluating nutrition education and social marketing programs with low-income audiences.

    J Nutr Educ. 2001; 33: S4-15

 

Continue Reading

Trending